The JUnit framework provides a great way to improve the robustness of a system over time, as the set of developers, maintainers, and even specifications of the system change. Through tests, you can check that certain invariants of your code are upheld.
Tests are usually broken up into two categories: unit and acceptance tests:
- Unit tests ensure that the constituent components act as they should.
- Acceptance tests ensure that the top-level functionality of the system, as it appears to a user, is as it should be.
JUnit can help with unit testing.
Ideally, unit tests developed for the system would completely cover the set of expected invariants over the constituent parts, giving new developers the ability to ensure that any changes they make will not break existing code.
Realistically, some of these invariants will be missed by the tests. This is partly because some invariants, while not at the level of full-scale system tests, involve the interaction of many separate components of the system.
In this article, I will discuss one such type of invariant and how sophisticated unit tests can be used to check it. The type of invariant I'm talking about is the proper order of invocations of a sequence of dependent methods.
Before continuing, it's important that you become acquainted with JUnit and learn how it can be used to easily write unit tests for your code. In the Resources section, I've included a link to all the information you will need to download and start using JUnit. (If you're familiar with JUnit, feel free to skip to the first example.)
Unit testing affords developers the following abilities:
- To design classes from an interface perspective
- To eliminate class clutter in the release package
- To automate validation to snare change bugs
The unit-testing process generally follows this path:
- Determine what your component should do.
- Formally (or informally, depending on the complexity) design your component.
- Write unit tests to check the behavior of your component. (The tests will not compile at this stage; the code is not yet written. The purpose for writing the test is to help focus on the intent of the component.)
- Code the component to the design; refactor as necessary.
- Stop the coding process when the tests (from step 3) pass.
- Brainstorm other code-breaking probabilities; write tests to confirm, then fix the code.
- Write a new test each time you detect a defect.
- Retest with all tests every time you change the code.
JUnit, a simple framework built by Erich Gamma and Kent Beck to write repeatable tests, makes it relatively simple to build an incrementally alterable test suite that can help developers measure development progress and detect unintended effects. JUnit is an instance of the xUnit architecture.
With JUnit, each test case extends the TestCase class. Each no-argument, public method in which the name starts with "test" is executed one at a time. The test methods call the component under test and make assertions about the behavior of that component. JUnit reports the location of each failed assertion.
JUnit is especially useful for the following reasons:
- It is a complete, open-source product; you don't have to write or purchase a framework.
- Because it is open source, plenty of users are experienced.
- It allows you to separate test code from product code.
- It is easily integrated in the build process.
Now that you've met JUnit, let's look at an example.
Consider the following example, one that handles the sending of various messages to some outside client:
public class Greeter {
public void sayHello() {...}
public void sayGoodbye() {...}
}
public class Sender {
public void sendFirstMessage() {...}
public void sendSecondMessage() {...}
}
public class Coordinator extends Thread {
Sender s;
Greeter g;
public Coordinator(Sender _s, Greeter _g) {
this.s = _s;
this.g = _g;
}
public void run() {
g.sayHello();
s.sendFirstMessage();
s.sendSecondMessage();
g.sayGoodbye();
}
}
|
The first class, Greeter, is responsible for establishing and breaking the connection to the outside client. The second class, Sender, is responsible for sending the various messages to the client. The third class, Coordinator, manages instances of the other two classes, ensuring that they work together to communicate with the client.
Needless to say, it's crucial that these methods are called in the appropriate order. But future extension and refactoring of the code may inadvertently change the order in which the methods are called. For example, another developer might move the invocation of the methods on Greeter and Sender into separate threads, using semaphores to regulate the order in which they're called.
How can we put a test into our suite to guarantee that the methods are called in the right order, no matter what changes take place? Unlike many unit tests, we can't just call these methods and check the result, because it is not the result of any one of them that we are trying to check.
Recording your next great hit...
The solution is to use a special type of Listener, one that I call a Recorder. Recorders keep a record of every method invocation on every object with which they are registered.
Recorders store these records linearly in the order in which they were called, much like a cassette tape. By installing the same Recorder into each of our objects, we can check the order of invocation on the methods in these objects. Consider the following code:
public class Recorder {
private StringBuffer tape;
public Recorder() {
this.tape = new StringBuffer();
}
public String playBack() {
return tape.toString();
}
public void record(String s) {
tape.append(s);
}
}
public class Greeter {
private Recorder r;
public Greeter(Recorder _r) {
this.r = _r;
}
public void sayHello() {
r.record("sayHello();");
...
}
public void sayGoodbye() {
r.record("sayGoodbye();");
...
}
}
public class Sender {
private Recorder r;
public Sender(Recorder _r) {
this.r = _r;
}
public void sendFirstMessage() {
r.record("sendFirstMessage();");
...
}
public void sendSecondMessage() {
r.record("sendSecondMessage();");
...
}
}
|
Notice that each method in Sender and Greeter must notify the Recorder of the new method invocation. In this way, Recorders are just like other kinds of Listeners: they must be notified when a change has occurred.
Also, notice that the message passed to the Recorder is a simple String. Using String messages has advantages and disadvantages. On the one hand, a more complex object could be stored at each method invocation, providing much more detailed information. On the other hand, such complex objects would make the job of testing much more difficult.
For example, using the Recorder from Listing 2, we can determine the order of invocations by adding the following simple test to our suite:
public void testOrderOfInvocation() throws InterruptedException {
Recorder r = new Recorder();
Greeter g = new Greeter(r);
Sender s = new Sender(r);
Coordinator c = new Coordinator(s, g);
c.start();
c.join();
assertEquals("sayHello();sendFirstMessage();sendSecondMessage();
sayGoodbye();",r.playBack());
}
|
Because we've stored the messages as simple Strings, the test to check the content of the playBack is easy: we just write out the String as it should be and check against that.
If, on the other hand, we had used a more complex type of object, we would have had to construct identical instances of each of these objects, and iterate through all the recorded instances, checking for equality on each of them. Additionally, this would require us to write equals methods for each class of recorded object.
That's a lot of work for a test case. I don't know about you, but I would rather spend my time writing more tests of a simpler kind (and designing the code to facilitate them) than writing infrastructure code for my tests.
One compromise between these two approaches would be to make a Recorder that stores very sophisticated data, but has a simple toString method that can be used for tests like the one above. The more sophisticated data could then be used in other tests to check the detailed properties of the sequence of invocations.
The idea of a Recorder for testing can be applied to many types of tests:
- In addition to checking simple order of invocation, Recorders can be used in distributed environments to ensure various invariants of communication are maintained during interprocess communication.
- Recorders can also be used with GUIs to ensure that responses to various user actions occur as expected.
In short, Recorders provide a means to test conglomerations of components that are bigger than what most unit tests cover, but still smaller than the entire system. I hope you find them as useful as I do.
- The JUnit home page offers a wealth of information on JUnit and related subjects.
- "Incremental development with Ant and JUnit" by Malcolm Davis (developerWorks, November 2000) discusses how to integrate these tools into your development environment.
- If you like JUnit, you might want to check out the entire set of xUnit testing tools for many different languages.
- The xUnit suite of tools is designed for use with Extreme Programming (XP), a new and powerful way of developing clean, robust software quickly.
- For a quick primer on XP, check out "XP distilled" by Roy Miller and Chris Collins (developerWorks, March 2001).
- "The UML Profile for Framework Architectures" (PDF slideshow) highlights a detailed case study of JUnit.
Eric Allen has an A.B. in computer science and mathematics from Cornell University. He is a Ph.D. candidate in the Java programming languages team at Rice University. His research concerns the development of semantic models and static analysis tools for the Java language, both at the source and bytecode levels. Currently, he is implementing a source-to-bytecode compiler for the NextGen programming language, an extension of the Java language with generic run-time types. Contact Eric at eallen@cs.rice.edu.
Comments (Undergoing maintenance)





