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