Fakes and remapping
Imagine we want to test an object-oriented application that has several
classes
that have has-a or uses relationships.
+---------------+
+-------+
+-----------+
+---------------+
| ATMSimulation |<>-----| Bank |<>-----|
Customer |<> -----| BankAccount |
+---------------+ +-------+
+-----------+
+---------------+
The classes are arranged in a hierarchy with ATMSimulation
aggregating Bank which aggregates Customer and so on.
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.
You create a Java interface for the dependent module and write a
fake class that
implements that interface.
Read Dependency
Injection & Testable Objects
or
Writing
More Testable Code with Dependency Injection
for a good introduction to the techniques.
Create a directory hierarchy for the source code that looks like this:
Source
|
+- ATM
| |
| +- ATMSimulation.java
| +- Bank.java
| +- Customer.java
| +- BankAccount.java
|
|
+- UnitTests
| |
| +--ATM
| |
| +-
TestATMSimulation.java
| +- TestBank.java
| +- TestCustomer.java
| +- TestBankAccount.java
|
+- Fakes
| |
| +--ATM
| |
| +-
Customer.java
| +- BankAccount.java
|
+- IntegrationTests
| |
| +--ATM
| |
| +-
TestATMSimulation.java
| +- TestBank.java
| +- TestCustomer.java
|
+- TestTools
|
+--SwingRobot
|
+--junit.jar
All the source code files must belong to the same Java package named
"ATM".
(See packages
tutorial).
Imagine multiple developers are creating this system and that a
different programmer is testing each class. How can this work if
all the development is in flux? You can't count on any low level
units being stable enough to test. The Bank programmer
doesn't want to wait until all the modules below him are finished
before
he can start working.
Testing the lowest level unit
The bottom level unit BankAccount is the easiest to test in
isolation because it doesn't use any other classes. The
programmer
puts the BankAccount.java file in the Source\ATM
directory. The JUnit class TestBankAccount.java goes in
the UnitTests\ATM directory.
Let's assume the junit.jar file is in a directory named TestTools
at the same level in the hierarchy as Source.
Compile the source module:
cd Source
javac -classpath .;TestTools\junit.jar
ATM\BankAccount.java
Compile the JUnit test:
javac -classpath .;UnitTests;TestTools\junit.jar
UnitTests\ATM\TestBankAccount.java
Note that because the JUnit test refers to the
BankAccount class the Source directory is included in the classpath.
Run the JUnit test:
java -classpath
.;UnitTests;TestTools\junit.jar
junit.textui.TestRunner ATM.TestBankAccount
Be sure to put the current directory in the
classpath (note the "dot").
Testing the upper level units with Fakes
Simultaneously another programmer is building the Customer
class. She puts the Customer.java file in the Source\ATM
directory and write a JUnit class TestCustomer.java and it
goes
in the UnitTests\ATM directory. She wants to test her Customer
class but BankAccount isn't ready yet. Instead of
waiting
for programmer one to finish she will create a Fake. She
grabs a copy of the original module skeleton for BankAccount
and adds a few dummy return values so that the class will execute and
return something. She places it in the Fakes\ATM directory.
Compile the source module:
cd Source
javac -classpath .;TestTools\junit.jar
ATM\Customer.java
Compile the JUnit test:
javac -classpath .;UnitTests;TestTools\junit.jar
UnitTests\ATM\TestCustomer.java
Compile the Fake low level unit and copy it to the UnitTest
directory:
javac -classpath
.;Fakes:TestTools\junit.jar Fakes\ATM\BankAccount.java
copy Fakes\ATM\BankAccount.class UnitTests\ATM
Run the JUnit test:
java -classpath
UnitTests;.;TestTools\junit.jar
junit.textui.TestRunner
ATM.TestCustomer
Important: The UnitTests directory must be placed in the
classpath
before the current directory because we want it to find the
fake
in the UnitTests directory rather than the "under development" BankAccount
in the Source directory.
The Bank class programmer uses the same technique. The Bank.java
file goes in the Source\ATM directory, the JUnit class TestBank.java
goes in the UnitTests\ATM directory. Write a fake Customer
file and place it in the Fakes\ATM directory.
Compile everything. Copy the fake Customer.class into
the UnitTests directory and run the test. We rely on
the
classpath ordering to cause the fake Customer to be used
instead of the real one under development.
Tip: It's a good habit to delete all your .class files
from the UnitTests directory before you commit anything to
the
svn repository so that someone else doesn't accidentally use your
fakes. On the other end, when you get source from svn, first
delete any lingering .class files for fakes left over by
other
developers.
Benefits
The big advantage of all this is that all the developers can work
simultaneously yet testing their own unit in isolation without using
anyone else's code. That way they can focus on verifying that
their unit is correct as an independent module before it has to
interact
with any other modules. Admittedly the hassle of managing the
directory structure and dealing with classpath is annoying but the
reward is not spending hours tracking down defects caused by another
developer's buggy code.
The big payoff for using fakes comes at integration time.
When all the units are ready to integrate, we simply delete all the
fake .class
files from the unit tests directory and then run the unit tests.
With the fakes gone, the TestRunner will use the real modules instead
and we have instant integration tests.
Example
Here is a zip
file with a complete example: directory hierarchy, source
code, unit tests, and integration tests. You can navigate around
the directory hierarchy to see where everything is located. You
can run the tests and observe the results. Highly recommended!
Note: The top-level directory in the zip file is named IntegrateDemo,
not
Source. You'll need to make the corresponding change to
the example commands shown above.
Exercise
The TestBank JUnit test is lacking any tests for the addCustomer()
and findCustomer() methods. Modify the TestBank.java file to
include a test for each of these methods. Run your test "in isolation"
and then as part of the integration test. Print your modified source
code and the results of both of your test runs.
Note that on Unix machines, change the backslash to a forward slash,
and change the semicolon separator character to a colon.
You may prefer some other way of arranging
the directories and managing the fakes but the general principle is to
cause the JUnit test to run with a fake class instead of a real
one. Whatever trick you can invent to make that happen is fine.
CPE 309
Home