Unit Testing and Mock Objects

Summary

Introduction

The first step in structuring the testing of a large program is unit testing (the remaining steps are discussed in Higher Order Testing chapter.) Unit testing is the lowest level of testing and it is the process of testing individual units within a program. In other words, rather than testing the whole program as a whole, testing is first focused on the smaller building blocks of the program. The motivations for unit testing are threefold:

  1. Unit testing is a way of managing the combined elements of testing since attention is initially focused on smaller units of the program.
  2. Unit testing eases the task of debugging.

  3. Unit testing introduces parallelism into the program testing process by allowing us to test multiple modules simultaneously.

Because unit testing is largely white-box oriented, the test-case design procedure for a unit test is the following: analyze the unit's logic using one or more of the while-box methods, and then supplement these test cases by applying black-box methods to the unit's specification.

Incremental Testing

When unit testing a large system, you are often faced with the following dilemma: should you test a program by unit testing each module independently and then combining the units to form the complete program, of should you combine the next module to be tested with the set of previously tested modules before it is tested? The first approach is known as non-incremental while the second is incremental.

The rectangles in the figure below represent units (modules) within a program. The lines connecting the units represent the control hierarchy of the program, in other words, unit A calls units B, C, and D. Unit B calls unit E and so on:

Note that unit testing each module requires what is known as a drive and one or more stubs. For example, to test unit B, test cases are first designed and then fed to unit B by passing it input arguments from a driver module, a small module that is coded to drive or execute the test cases. In most cases, the drive is NUnit. In addition, because unit B calls unit E, something must be present to receive control when unit B calls unit E. Mock objects (discussed below) accomplish this requirement.

In non-incremental testing, a unit test is performed on each of the six units. The units are then combined or integrated to form the program. In incremental testing, the next unit to be tested is first combined with the set of units that have already been tested before testing the unit. It is worth mentioning that incremental testing is superior to nonincremental testing for the following reasons.

A key issue is whether we should begin testing from top to bottom or from bottom to top:

The following table summarizes the relative advantages and disadvantages of top-down and bottom-up testing.

Top-Down Testing

Advantages Disadvantages
Useful if major flaws occur toward the top of the program. Mock unit must be produced.
Once I/O functions are added, representations of test cases is easier. Mock units are often complicated.

Early skeletal program allows demonstrations.

Before the I/O functions are added, the representations of test cases in mocks can be difficult.
  Test conditions may be difficult to create.
  Observation of test output is more difficult.
  Allows one to think that design and testing can be overlapped.

Bottom-Up Testing

Advantages Disadvantages

Useful if major flaws occur toward the bottom of the program.

Drivers must be produces (not a problem when using NUnit)
Test conditions are easier to create. The program as an entity does not exist until the last unit is integrated.
Observation of test results is easier.  

Theory of Mock Objects

Sometimes it is impossible to test a single method on a single object without having some kind of state built around it. This usually happens when the object being tested needs to interact with other objects. An example would be a business object that interacts with a database via a data access object. Mock objects provide a way out of this dilemma, and as an added benefit, can help enforce good design practices. A mock data access object conforms to the interface of the real data access object, but has just enough code to fool the tested object and track its behaviour.

An essential aspect of unit testing is to test only one thing at a time. Test code should communicate its intent simply and clearly, but can be difficult if a test has to set up a domain state or the domain code causes side effects. For example, going back to the business object example, a data access object is necessary for the business object to perform its function. A mock object for the data access object can be initialized with state relevant to the test and can validate the inputs received from the unit test.

There are two important notes to point here: 1) The mock object does not test the data access object, and 2) we are not trying to rewrite the data access object, only to reproduce those responses that we need for a particular test. Most of the methods in of a mock implementation do nothing or just stored values in member variables.

A mock object should expose the same interface as the real object, but should not duplicate its implementation. It should allow you to set up private state to aid in testing. In mock implementations, the emphasis in on simplicity rather than on completeness. For example, a mock class for a data access object might always return the same data set regardless of the actual query.

Advantages of Mock Objects

Localizing Unit Tests

Mock objects help defer infrastructure choices. For example, you may wish to write a business object without committing to a particular database. Until a choice is made, we can write a mock class that provides the minimum behaviour that we would expect from our database. We can continue writing tests for our application code without waiting for a working database. The mock code also gives us an initial definition of the required functionality from the database.

Mock objects also help coping with scale. A unit test that depends on complex system state can be difficult to set up, especially as the rest of the system develops. Mock objects avoid these situations by providing a lightweight emulation of the requires system state. The setup of complex state is localized to one mock object, instead of being scattered throughout many unit tests.

Better Tests

A mock implementation can test assertions each time it interacts with a domain code, and so is more likely to fail at the right time and generate a useful error message. For example, a mock data access object knows that it should open the connection only once and can fail as soon as the connection is opened for the second time:

public class DataAccessMock : IDataAccess        // IDataAccess is implemented by the real object
{
    public void OpenConnection()
    {
        if (m_Connection.State == Opened)
            Assert.Fail( "Connection is already open" );
       
        ...
    }

    ...
}

When testing without mock objects, each unit test tends to have its own set of assertions about the domain code. These may be refactored into helper (shared) methods in the unit test, but the developer must remember to apply them to new unit tests. On the other hand, these assertions can be built into mock objects and are therefore applied by default whenever the mock object is used.

Interface Discovery

Developing with mock objects is often a good technique for discovering the interface of the real object. As domain objects and their mock objects stabilize, we can extract their interactions to define new interfaces that the system must implement. An interface will consist of those methods of a mock object that are not involved in setting or checking expectations.

Inversion of Control (Dependency Injection)

Basic Concept

Inversion of Control is a fancy name for a very simple but elegant way of decoupling classes from each other. In fact, Inversion of Control IOC encourages better software design by encouraging 1) reuse, 2) loose coupling, and 3) easy testing of software.

The main concept is as follows: Assume class A uses the services of class B as follows:

// A has tight coupling with B
public class A
{
    private B b;
    public A()
    {
        b = new B;                // A creates B. What if B changes?
    }

    public void DoSomething()
    {
        b.GetSomething();
    }
}

The fact that class A instantiates class B creates a tight coupling between A and B. You cannot easily change B without changing A. To eliminate this coupling, a client (often called a configurator) injects and instance of class B to A via A's constructor. So the control of how object a gets the reference of object b has been inverted. Object a is not responsible for instantiating object b. Instead the configurator is now responsible for it.

To illustrate how A is tightly coupled on B, assume now that B needs to take an object of type C as as constructor argument:

// A has tight coupling with B
public class A
{
    private B b;
    public A()
    {
        b = new B( new C() );      // We had to change B for new requirements so A had to change as well.
    }

    // ...

}

Now object a owns both object b and object c. If class B or class C changes at all, then A needs to change again. A simple design of class A could become a maintenance nightmare.

What we want is to loosen the coupling between A and B by shifting the responsibility of creating B to a third party entity (a configurator). This insulates A from changes in class B. Consider this design of class which uses IOC:

public class A
{
    private B b;
    public class A(B ob )
    {
        b = ob;
    }

    // ...

}

The listing above assumes the following design decisions:

Implementing IOC

The IOC pattern presented above can in fact be implemented in three ways; constructors, setters, and interfaces: