Skip to main content

Diagnosing Java Code: Recorders test for proper method invocation

Writing Recorders for unit tests that ensure method invocations occur in the proper order

Eric Allen (eallen@cs.rice.edu), Ph.D. candidate, Java programming languages team, Rice University
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.

Summary:  Unit testing with JUnit can be a powerful way to ensure the integrity of your code base, but some of the invariants are trickier to test than others -- method-invocation sequence is one of them. In this installment of Diagnosing Java Code, Eric Allen describes how to use Recorders (a special type of Listener) in your unit tests to ensure that a sequence of method invocations occurs in the proper order. Share your thoughts on this article with the author and other readers in the discussion forum by clicking Discuss at the top or bottom of the article.

View more content in this series

Date:  01 Jun 2001
Level:  Introductory
Activity:  1613 views

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.

Fast-track to the code

Listing 1. Three classes to communicate with clients
The Greeter class establishes and breaks the connection to the outside client. The Sender class sends the various messages. The Coordinator class manages instances of the other two classes, ensuring that they work together.

Listing 2. Installing the same Recorder into each object
By installing the same, special Listener (called a Recorder) into each of the objects, you can determine the order of invocation on the methods in these objects.

Listing 3. A JUnit test
By storing messages as simple Strings, this simple test to check the content of the playBack is all we need, instead of having to build a test to check every possible iteration of recorded instances.

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.

Shake hands with JUnit

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:

  1. Determine what your component should do.

  2. Formally (or informally, depending on the complexity) design your component.

  3. 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.)

  4. Code the component to the design; refactor as necessary.

  5. Stop the coding process when the tests (from step 3) pass.

  6. Brainstorm other code-breaking probabilities; write tests to confirm, then fix the code.

  7. Write a new test each time you detect a defect.

  8. 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.


Greeters and Senders

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();");
         ...
     }
 }



To String or not toString

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.


Ready to test

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.


Resources

About the author

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)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10550
ArticleTitle=Diagnosing Java Code: Recorders test for proper method invocation
publish-date=06012001
author1-email=eallen@cs.rice.edu
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers