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 |
+----------------+ +------------------------+
+---------------+
The purpose of this application is to pack a suitcase with items of
different weights without exceeding the weight limit for airline
checked baggage. 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.io.File;
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 song)
{
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
playlist
size
is
empty
assertEquals(0,packer.numItems());
//
song
in
position
2
is
too
big
assertFalse(packer.addToCase(2));
//
add
a
song
that
fits
assertTrue(packer.addToCase(1));
//
Verify
playlist
now
has
one
song
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());
}
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 EasyMock
EasyMock gives you the benefits of dependency injection without having to write
fakes. Yowza!
The setup is much like
approach two, above:
you create a Java interface for the lower
level module.
But Easy Mock simplifies the testing process because you don't have to
actually create a separate fake class.
Here's an introductory slide
show,
and a tutorial
article.
CPE 309
Home