Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use unit testing when classes depend on one another or external data?

I'd like to start using unit tests, but I'm having a hard time understanding how I can use them with my current project.

My current project is an application which collects files into a 'Catalog'. A Catalog can then extract information from the files it contains such as thumbnails and other properties. Users can also tag the files with other custom meta data such as "Author" and "Notes". It could easily be compared to a photo album application like Picasa, or Adobe Lightroom.

I've separated the code to create and manipulate a Catalog into a separate DLL which I'd now like to test. However, the majority of my classes are never meant to be instantiated on their own. Instead everything happens through my Catalog class. For example there's no way I can test my File class on its own, as a File is only accessible through a Catalog.

As an alternative to unit tests I think it would make more sense for me to write a test program that run through a series of actions including creating a catalog, re-opening the catalog that was created, and manipulating the contents of the catalog. See the code below.

//NOTE: The real version would have code to log the results and any exceptions thrown

//input data
string testCatalogALocation = "C:\TestCatalogA"
string testCatalogBLocation = "C:\TestCatalogB"
string testFileLocation = "C:\testfile.jpg"
string testFileName = System.IO.Path.GetFileName(testFileLocation);


//Test creating catalogs
Catalog catAtemp = Catalog(testCatalogALocation)
Catalog catBtemp = Catalog(testCatalogBLocation );


//test opening catalogs
Catalog catA = Catalog.OpenCatalog(testCatalogALocation);
Catalog catB = Catalog.OpenCatalog(testCatalogBLocation );


using(FileStream fs = new FileStream(testFileLocation )
{
    //test importing a file
    catA.ImportFile(testFileName,fs);
}

//test retrieving a file
File testFile = catA.GetFile(System.IO.Path.GetFileName(testFileLocation));

//test copying between catalogs
catB.CopyFileTo(testFile);


//Clean Up after test
System.IO.Directory.Delete(testCatalogALocation);
System.IO.Directory.Delete(testCatalogBLocation);

First, am I missing something? Is there some way to unit test a program like this? Second, is there some way to create a procedural type test like the code above but be able to take advantage of the testing tools building into Visual Studio? Will a "Generic Test" in VS2010 allow me to do this?


Update

Thanks for all the responses everyone. Actually my classes do in fact inherit from a series of interfaces. Here's a class diagram for anyone that is interested. Actually I have more interfaces then I have classes. I just left out the interfaces from my example for the sake of simplicity.

Thanks for all the suggestions to use mocking. I'd heard the term in the past, but never really understood what a "mock" was until now. I understand how I could create a mock of my IFile interface, which represents a single file in a catalog. I also understand how I could create a mock version of my ICatalog interface to test how two catalogs interact.

Yet I don't understand how I can test my concrete ICatalog implementations as they strongly related to their back end data sources. Actual the whole purpose of my Catalog classes is to read, write, and manipulate their external data/resources.

like image 314
Eric Anastas Avatar asked Sep 08 '10 03:09

Eric Anastas


2 Answers

You ought to read about SOLID code principles. In particular the 'D' on SOLID stands for the Dependency Injection/Inversion Principle, this is where the class you're trying to test doesn't depend on other concrete classes and external implementations, but instead depends on interfaces and abstractions. You rely on an IoC (Inversion of Control) Container (such as Unity, Ninject, or Castle Windsor) to dynamically inject the concrete dependency at runtime, but during Unit Testing you inject a mock/stub instead.

For instance consider following class:

public class ComplexAlgorithm
{
    protected DatabaseAccessor _data;

    public ComplexAlgorithm(DatabaseAccessor dataAccessor)
    {
        _data = dataAccessor;
    }

    public int RunAlgorithm()
    {
        // RunAlgorithm needs to call methods from DatabaseAccessor
    }
}

RunAlgorithm() method needs to hit the database (via DatabaseAccessor) making it difficult to test. So instead we change DatabaseAccessor into an interface.

public class ComplexAlgorithm
{
    protected IDatabaseAccessor _data;

    public ComplexAlgorithm(IDatabaseAccessor dataAccessor)
    {
        _data = dataAccessor;
    }

    // rest of class (snip)
}

Now ComplexAlgorithm depends on an interface IDatabaseAccessor which can easily be mocked for when we need to Unit test ComplexAlgorithm in isolation. For instance:

public class MyFakeDataAccessor : IDatabaseAccessor
{
    public IList<Thing> GetThings()
    {
        // Return a fake/pretend list of things for testing
        return new List<Thing>()
        {
            new Thing("Thing 1"),
            new Thing("Thing 2"),
            new Thing("Thing 3"),
            new Thing("Thing 4")
        };
    }

    // Other methods (snip)
}

[Test]
public void Should_Return_8_With_Four_Things_In_Database()
{
    // Arrange
    IDatabaseAccessor fakeData = new MyFakeDataAccessor();
    ComplexAlgorithm algorithm = new ComplexAlgorithm(fakeData);
    int expectedValue = 8;

    // Act
    int actualValue = algorithm.RunAlgorithm();

    // Assert
    Assert.AreEqual(expectedValue, actualValue);
}

We're essentially 'decoupling' the two classes from each other. Decoupling is another important software engineering principle for writing more maintainable and robust code.

This is really the tip of the tip of the iceberg as far as Dependency Injection, SOLID and Decoupling go, but it's what you need in order to effectively Unit test your code.

like image 60
Sunday Ironfoot Avatar answered Sep 17 '22 18:09

Sunday Ironfoot


Here is a simple algorithm that can help get you started. There are other techniques to decouple code, but this can often get you pretty far, particularly if your code is not too large and deeply entrenched.

  1. Identify the locations where you depend on external data/resources and determine whether you have classes that isolate each dependency.

  2. If necessary, refactor to achieve the necessary insulation. This is the most challenging part to do safely, so focus on the lowest-risk changes first.

  3. Extract interfaces for the classes that isolate external data.

  4. When you construct your classes, pass in the external dependencies as interfaces rather than having the class instantiate them itself.

  5. Create test implementations of your interfaces that don't depend on the external resources. This is also where you can add 'sensing' code for your tests to make sure the appropriate calls are being used. Mocking frameworks can be very helpful here, but it can be a good exercise to create the stub classes manually for a simple project, as it gives you a sense of what your test classes are doing. Manual stub classes typically set public properties to indicate when/how methods are called and have public properties to indicate how particular calls should behave.

  6. Write tests that call methods on your classes, using the stubbed dependencies to sense whether the class is doing the right things in different cases. An easy way to start, if you already have functional code written, is to map out the different pathways and write tests that cover the different cases, asserting the behavior that currently occurs. These are known as characterization tests and they can give you the confidence to start refactoring your code, since now you know you're at least not changing the behavior you've already established.

Best of luck. Writing good unit tests requires a change of perspective, which will develop naturally as you work to identify dependencies and create the necessary isolation for testing. At first, the code will feel uglier, with additional layers of indirection that were previously unnecessarily, but as you learn various isolation techniques and refactor (which you can now do more easily, with tests to support it), you may find that things actually become cleaner and easier to understand.

like image 29
Dan Bryant Avatar answered Sep 20 '22 18:09

Dan Bryant