Unit Testing Asynchronous Code

I try to avoid using code that instantiates threads from unit-tests. They’re awkward to write, brittle and I would rather extract the controlling thread, using the test as the controller. However there are times when it’s unavoidable.

Here’s an example. A PriceFinder is a class that goes and retrieves a price for a symbol asyncronously, returning immediately. Sometime later it receives a response and performs a callback with the result. I’ve left out the implementation of how it actually does this (maybe a web-service or database call).

public class PriceFinder {
  public void findPrice(String symbol, PriceCallback callback) { ... }
}

public interface PriceCallback {
  void foundPrice(Price price);
}

To test this, a callback can be used from the test case that records what is passed in. The test can then wait for a specified time and assert that the correct result is received. If the asyncronous code does not complete within the specified time, the test will fail (suggesting the code is either running very slowly or is never going to complete).

public class PriceFinderTest extends TestCase {

  private PriceFinder finder = new PriceFinder();
  private Price receivedPrice;

  public void testRetrievesAValidPrice() throws Exception {

    // Start the asynchronous call
    finder.findPrice("MSFT", new PriceCallback() {
      public void foundPrice(Price price) {
        // Callback stores the result for test
        receivedPrice = price;
      }
    });

    // Wait two seconds (URGGGGGH)
    Thread.sleep(2000);

    // Assert that the result was received
    assertNotNull("Expected a price", receivedPrice);
  }

}

However, this sucks as it will slow your test suite right down if you have loads of tests using Thread.sleep(). It’s also somewhat brittle. Is 2 seconds enough? If it takes longer the test will fail. Extending it will make the tests even slower.

A less time consuming way to do it is by using wait() and notify() on a lock. The handler notifies the lock and the test waits for this notification. In case this notification never happens, a timeout is used when calling wait().

public class PriceFinderTest extends TestCase {

  private PriceFinder finder = new PriceFinder();
  private Price receivedPrice;

  private Object lock = new Object(); // <--- Coordination lock

  public void testRetrievesAValidPrice() throws Exception {

    // Start the asynchronous call
    finder.findPrice("MSFT", new PriceCallback() {
      public void foundPrice(Price price) {
        receivedPrice = price;
        synchronized(lock) {
          lock.notify();
        }
      }
    });

    // Wait two seconds or until the monitor has been notified.
    synchronized(lock) {
      lock.wait(2000);
      // But there's still a problem...
    }

    assertNotNull("Expected a price", receivedPrice);
 }

}

This optimistic approach results in fast running tests while all is good. If the PriceFinder is behaving well, the test will not wait any longer than the PriceFinder takes to complete its work. If PriceFinder has a bug in it and never calls the handler, the test will fail in at most two seconds.

However, there’s still a subtle issue. In the case that the PriceFinder is really fast, it may call notify() before the test starts wait()ing. The test will still pass, but it will wait until the timeout occurs.

This is where threading and synchronization get messy and beyond me.

Doug Lea has a nice little class called Latch in his concurrency library (available in JDK 1.5 as java.util.concurrent.CountDownLatch).

A latch can only be locked once, once released it will never lock again.

public class PriceFinderTest extends TestCase {

  private PriceFinder finder = new PriceFinder();
  private Price receivedPrice;

  private Latch latch = new Latch();

  public void testRetrievesAValidPrice() throws Exception {
    finder.findPrice("MSFT", new PriceHandler() {
      public void foundPrice(Price price) {
        receivedPrice = price;
        latch.release();
      }
    });

    // Wait until the latch is released or a timeout occurs.
    // Importantly: if the latch has already been released,
    // proceed immediately.
    latch.attempt(2000);

    assertNotNull("Expected a price", receivedPrice);
  }
}

That’ll do it. Fast, simple, and robust.