How to Test Units in Isolation

The main purpose of unit testing is to verify that an individual unit (a class, in Java) is working correctly before it is combined with other components in the system.   For the lowest level units in the class hierarchy that have no dependencies, this is easy. A driver or JUnit test can exercise the unit by itself. But what about for upper level units that depend on lower level units? How can we test a unit in isolation without requiring any of its dependencies?


For example, say we want to test an object-oriented application that has several classes that have has-a or uses relationships.

+----------------+       +------------------------+        +---------------+
| SuitcasePacker |<>-----| WeightConstrainedList  |<> -----|   TravelGear  |
+----------------+       +------------------------+        +---------------+

It's important that each unit be tested by itself so we can be confident in its correctness before we integrate it with other units.   Having high quality individual units is the most important step in having a smooth and painless integration. 

It's also important to be able to exploit parallelism in the development;  The developer of SuitcasePacker doesn't want to wait until all the modules below him are finished before he can start working.  We want to give him a way he can unit test SuitcasePacker even thought the other modules aren't yet completed.


The purpose of this application is to pack a suitcase with items of different weights without exceeding the weight limit for a pack mule.   SuitcasePacker represents a tool that can fill a suitcase from a list of desired items to take traveling.   WeightConstrainedList is a list that limits items to a total weight. TravelGear is an item to put in the suitcase.  How can we test SuitcasePacker in isolation?  The essence of the technique is to create a "fake" list that will take the place of WeightConstrainedList. A fake or a "stub" is a skeleton implementation of some dependent class that provides just a minimal amount of functionality to support testing of the target class. In this case FakeWeightConstrainedList provides just the minimum implementation required to create a unit test for SuitcasePacker.

There are three several approaches to arranging the classes to facilitate this style of unit testing:

Approach 1: Fakes via duplication

The simplest strategy is to create FakeWeightConstrainedList by duplicating the interface of the WeightConstrainedList and placing the fake in the same folder at the original class.  SuitcasePacker hardcodes an instantiation of the fake.  (Assume TravelGear is complete and unit-tested.)
package suitcaseisolationtesting;

import java.util.ArrayList;
/**
 * SuitcasePacker represents a tool that can fill a suitcase
 * from a list of desired items to take traveling.
 * In this version, the "Fake" dependency name is hardcoded into the constructor,
 * which is a simple approach, but has the downside of requiring recompiling the
 * class to replace the name with the real dependency for integration testing.
 *
 */
public class SuitcasePacker
{
    private FakeWeightConstrainedList suitcase;
    private ArrayList<TravelGear> packableItems;

    /** Constructor for objects of type Suitcase */
    public SuitcasePacker(ArrayList<TravelGear> desiredItems)
    {
        suitcase = new FakeWeightConstrainedList();
        packableItems = desiredItems;
    }
    /** Attempt to put an item in the suitcase, subject to a weight limit.
     * @param position the zero-based index of the item to be moved.
     * @return true if the item was added to the suitcase, false if adding
     * the item would have exceeded the current weight limit.
     */
    public boolean addToCase(int position)
    {
        // Is there room in the suitcase for another item?
        if (suitcase.notFull(packableItems.get(position)))
        {
            suitcase.add(packableItems.remove(position));
            return true;
        }
        return false;
    }
    /** Returns the number of items in the suitcase.
     * @return the number of items in the suitcase.
     */
    public int numItems()
    {
        return suitcase.size();
    }
}

The fake class is very minimal - it doesn't even have a list. 

package suitcaseisolationtesting;

import java.io.*;
/**
 * Fake implementation to support unit testing.
 */
public class FakeWeightConstrainedList
{
    private int size;

    public FakeWeightConstrainedList()
    {
        super();
        size = 0;
    }
   
    public boolean notFull(TravelGear newItem)
    {
        return newItem.getWeight() == 1;
    }

    public boolean add(TravelGear gear)
    {
        size = 1;
        return true;
    }

    public int size()
    {
        return size;
    }

}
The unit test cleverly sets up the test data to correspond to what the fake is expecting.
    public void testAddToCase()
    {
        System.out.println("unit test OR integration test of addToCase");
        ArrayList<TravelGear> desiredGear = new ArrayList<TravelGear>();
        desiredGear.add(new TravelGear("A",1)); // position zero
        desiredGear.add(new TravelGear("B",1)); // one
        desiredGear.add(new TravelGear("C",46)); // two

        SuitcasePacker packer = new SuitcasePacker(desiredGear);
        // check that suitcase is empty
        assertEquals(0,packer.numItems());
        // gear in position 2 is too big
        assertFalse(packer.addToCase(2));
        // add a gear that fits
        assertTrue(packer.addToCase(1));
        // Verify suitcase now has one gear
        assertEquals(1,packer.numItems());
        assertEquals(2,desiredGear.size());
        // verify the proper gear was removed from desiredGear
        assertEquals("A",desiredGear.get(0).toString());
        assertEquals("C",desiredGear.get(1).toString());
    }

The advantage of this approach is that a carefully designed unit test will also pass when run during integration.   The downside is that it requires recompiling SuitcasePacker for integration testing to replace the fake name with the real dependency.  This is a reasonably serious drawback because it precludes a full automated test suite.  You can't run all the unit tests and all the integration tests at the same time.

Another drawback is if two or more developers need a fake with different behavior, it can become awkward. For example, in the scenario above, maybe SuitcasePacker can be tested with a simple FakeWeightConstrainedList, but if some other class in the system needs a more complex fake then you have to create two fake classes and give them slightly different names (e.g., FakeWeightConstrainedListOne and FakeWeightConstrainedListTwo).

What we've accomplished is that we can write complete unit tests for SuitcasePacker and verify that it operates correctly as an entirely separate unit without needing any other dependent modules to be working.   This improves productivity by allowing parallel development, and increases quality by having thoroughly unit tested modules.

Zip file for example code

Approach 2: Dependency Injection

The essence of this approach is modifying the design of SuitcasePacker so that the dependent class can be passed as a parameter to the constructor ("injected").  This is done by extracting the interface of the original dependent class and create a fake implementation with stub methods.  

Read Dependency Injection & Testable Objects or Writing More Testable Code with Dependency Injection for a good introduction to this techniques.

What this does is parameterizes the component that changes between unit testing and integration testing, thus isolating SuitcasePacker from change.  Now SuitcasePacker does not have to be recompiled between test phases and a fully automated test suite can be built.

Here's the interface extracted from the dependent class.
package mixtapeisolationtesting;

/**
 * Interface for a list that is constrained by a weight limit.
 * Items can be added and removed from the list and the list
 * automatically tracks the available weight left in the list.
 *
 * @author jdalbey
 */
public interface I_WeightConstrainedList
{
    /**
     * Default weight limit in pounds
     */
    int kDefaultWeightLimit = 45;

    /**
     * Accessor to the available weight remaining in the list,
     * that is, unused weight.
     * @return remaining weight
     */
    int getRemainingWeight();

    /**
     * Adds up the total weight taken by the items in this list.
     * @return int total weight
     */
    int getTotalWeight();

    /**
     * Determine if the list has room for a specific piece of gear.
     * I.e., if this item were added to the list, would it
     * exceed the list's capacity?
     * @ return true if the item can fit, false otherwise.
     */
    boolean notFull(TravelGear newItem);

    /**
     * Appends the specified element to the end of this list.
     * @param newItem - element to be appended to this list.
     * @return true (as per the general contract of Collection.add).
     */
    boolean add(TravelGear newItem);

    /**
     * Returns the number of elements in this list.
     * @return the number of elements in this list.
     */
    int size();
}
Here's how the constructor is modified:
    private I_WeightConstrainedList suitcase;
    private ArrayList<TravelGear> packableItems;

    /** Constructor for objects of type Suitcase */
    public SuitcasePacker(ArrayList<TravelGear> desiredItems, I_WeightConstrainedList bag)
    {
        suitcase = bag;
        packableItems = desiredItems;
    }
Now the unit test creates an instance of the fake and passes it to the class under test:
        I_WeightConstrainedList fake = new FakeWeightConstrainedList();
        SuitcasePacker packer = new SuitcasePacker(desiredGear,fake);
A separate integration test creates an instance of the real dependent class and passes it to the class under test:
        I_WeightConstrainedList real = new WeightConstrainedList();
        SuitcasePacker packer = new SuitcasePacker(desiredGear,real);
Now if a second developer needs a slightly different fake, they can create a private inner class in their unit test that overrides the methods they want to customize.
    public void testAddToCase()
    {
        System.out.println("unit test ONLY of addToCase");
        ArrayList<TravelGear> desiredGear = new ArrayList<TravelGear>();
        desiredGear.add(new TravelGear("A",1)); // position zero
        desiredGear.add(new TravelGear("B",10)); // one
        desiredGear.add(new TravelGear("C",46)); // two
        // Replace this with the real class for integration testing.
        I_WeightConstrainedList fake = new Fake2WeightConstrainedList();
        SuitcasePacker packer = new SuitcasePacker(desiredGear,fake);
        ...
    }

class Fake2WeightConstrainedList extends FakeWeightConstrainedList
{
    public boolean notFull(TravelGear newItem)
    {
        return newItem.getWeight() == 10;
    }
}
Zip file for second example code

Approach 3: Dependency Injection with remapping

This approach puts all the fakes in a separate directory.  This reorganizational scheme reduces clutter in the unit test folder and allows you to give fakes the same name as the real classes.   This can be useful in special situations where the unit test is also going to function as the integration test. 

Link to fakes with remapping.

Approach 4: Dependency Injection with JMock

JMock gives you the benefits of dependency injection without having to write fakes.  Hurray!
The setup is much like approach 2, above: you create a Java interface for the lower level module.
But JMock simplifies the testing process because you don't have to actually create a separate fake class. 
Read the Getting Started page and have a look at the Cheat Sheet quick reference.


package suitcaseisolationtesting;

import java.util.ArrayList;
import junit.framework.TestCase;
import org.jmock.Mockery;
import org.jmock.Expectations;

public class SuitcasePackerUnitTest extends TestCase {
        Mockery context = new Mockery();

    public SuitcasePackerUnitTest(String testName) {
        super(testName);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
    }

    public void testAddToCase()
    {
        System.out.println("unit test ONLY of addToCase");
        ArrayList<TravelGear> desiredGear = new ArrayList<TravelGear>();
        final TravelGear item0 = new TravelGear("A",1);
        final TravelGear item1 = new TravelGear("B",1);
        final TravelGear item2 = new TravelGear("C",46);
        desiredGear.add(item0); // position zero
        desiredGear.add(item1); // one
        desiredGear.add(item2); // two
        final I_WeightConstrainedList fake = context.mock(I_WeightConstrainedList.class);
        SuitcasePacker packer = new SuitcasePacker(desiredGear,fake);
        context.checking(new Expectations() {{
                oneOf (fake).size(); will(returnValue(0));
                oneOf (fake).notFull(item2); will(returnValue(false));
                oneOf (fake).notFull(item1); will(returnValue(true));
                oneOf (fake).add(item1); will(returnValue(true));
                oneOf (fake).size(); will(returnValue(1));
                never (fake).add(item0); will(returnValue(true));
                never (fake).add(item2); will(returnValue(false));
        }});
        // check that playlist size is empty
        assertEquals(0,packer.numItems());
        // item in position 2 is too big
        assertFalse(packer.addToCase(2));
        // add an item that fits
        assertTrue(packer.addToCase(1));
        // Verify suitcase now has one item
        assertEquals(1,packer.numItems());
        assertEquals(2,desiredGear.size());
        // verify the proper item was removed from desiredGear
        assertEquals("A",desiredGear.get(0).toString());
        assertEquals("C",desiredGear.get(1).toString());
        context.assertIsSatisfied();
    }


}
Zip file for JMock version


Approach 5: Stubbing with Mockito

An even simpler mocking framework is Mockito. It is less powerful than JMock but for most situations students encounter it will be adequate. It's much easier to use since it doesn't require Interfaces or dependency injection. It has a few limitations: cannot mock final classes, static methods, or equals(), hashCode() (which you shouldn't do anyway). Below is the same unit test as the JMock example above.
package suitcaseisolationtesting;

import java.util.ArrayList;
import junit.framework.TestCase;
import static org.mockito.Mockito.*;

public class SuitcasePackerTest extends TestCase {
    
    public SuitcasePackerTest(String testName) {
        super(testName);
    }

    ArrayList<TravelGear> desiredGear;

    /**
     * Test of addToCase method, of class SuitcasePacker.
     */
    public void testAddToCase()
    {
        System.out.println("test addToCase");
        desiredGear = new ArrayList<TravelGear>();
        final TravelGear item0 = new TravelGear("A",1);
        final TravelGear item1 = new TravelGear("B",1);
        final TravelGear item2 = new TravelGear("C",46);
        desiredGear.add(item0); // position zero
        desiredGear.add(item1); // one
        desiredGear.add(item2); // two

        WeightConstrainedList gearlist = mock(WeightConstrainedList.class);
        SuitcasePacker packer = new SuitcasePacker(desiredGear,gearlist);

        // check that playlist size is empty
        when(gearlist.size()).thenReturn(0);
        assertEquals(0,packer.numItems());
        // item in position 2 is too big
        when(gearlist.notFull(item2)).thenReturn(false);
        assertFalse(packer.addToCase(2));
        // add an item that fits
        when(gearlist.notFull(item1)).thenReturn(true);
        assertTrue(packer.addToCase(1));
        // Verify suitcase now has one item
        when(gearlist.size()).thenReturn(1);
        assertEquals(1,packer.numItems());
        assertEquals(2,desiredGear.size());
        // verify the proper item was removed from desiredGear
        assertEquals("A",desiredGear.get(0).toString());
        assertEquals("C",desiredGear.get(1).toString());
    }

}



Zip file for Mockito version

mockito-1.10.19.jar (without source code)


CPE 309 Home