1. 程式人生 > >TDD, Unit Tests and the Passage of Time

TDD, Unit Tests and the Passage of Time

Many programmers have a hard time writing good unit-tests for code that involves time. For example, how do you test time-outs, or periodic clean-up jobs? I have seen many tests that create elaborate set-ups with lots of dependencies, or introduce real time gaps, just to be able to test those parts. However, if you structure the code the right way, much of the complexity disappears. Here is an example of a technique that lets you test time-related code with ease.

Make Time External

The key is to make time external to the code you are testing. For example, at work recently, I was writing code to collect several parts of a text message (SMS). The parts arrive independently, and when all parts have been collected, the complete message (all parts) is delivered. Of course, all parts may not arrive (due to for example network problems), so there is a need for a time out. The data structure I used is basically an array in which each arriving part is stored. When the last part arrives, the complete message is delivered, and the array is released. However, if all parts have not arrived in 30 seconds, the parts that have arrived should be delivered anyway, and the array is released.

The logic is implemented in the class ConcatInMemoryHandler. In addition to a collectPart() method, it has a tick() method:

public void tick() {
  ticks++;
  checkForConcatTimeout(ticks);
}

The tick() method is called every second. It increments the ticks counter and then checks for time outs. When the first part of a message arrives, the current value of ticks

is stored with the array. If the time out is set to 30 seconds, the checkForConcatTimeout() only needs to compare the current value of ticks (passed in as the only argument) to the stored value. If the difference is greater than 30, there is a time out.

To make testing even easier, the time out value is set in the constructor of the ConcatInMemoryHandler. By setting it to 1 instead of 30, you only have to call tick() twice to cause a time out. Here is an example of a test:

public void testNotAllParts_timeOut() {
  // Adding 2 of 4 parts (two parts missing).
  // Should time out (with the parts received).
  ConcatInMemoryHandler handler;
  handler = new ConcatInMemoryHandler(1, user);
  List<SubmitImpl> submits = makeSubmits(4);
  handler.collectPart(submits.get(3), user);
  handler.collectPart(submits.get(1), user);
  assertEquals(1, handler.getPartsHashMapSize());

  // now time out without two of the parts
  handler.tick();
  handler.tick();
  assertEquals(0, handler.getPartsHashMapSize());
  assertEquals(2, user.timedoutSubmits.size());
}

Advantages

There are several advantages to letting time be external and only passing it in as an argument:

  1. No real time gaps in the tests – letting 5 seconds pass is simply a matter of calling tick() 5 times.
  2. No need to call System.currentTimeMillis(), sleep() or similar.
  3. No need to start other threads.
  4. It is easy to use a short time out – just configure a lower value for the test.

There are no real disadvantages. All you have to do is make sure the handler is created with the correct time-out value, and make sure tick() gets called every second. This is actually an example of There Are Only Two Roles of Code that John Sonmez wrote about –  algorithms and coordinators. The ConcatInMemoryHandler is the algorithm, and coordination is done by the code that creates the handler and calls tick() on it periodically. This is a very useful way of thinking about (and writing) programs, and John’s article is a great read.

This way of structuring the code is a natural result of using test-driven development (TDD).  Because you make an effort to structure the code in a way that makes it testable, you end up separating the concerns. The resulting structure, more than the tests themselves, is the biggest benefit of using TDD.

A small note on the test above. Normally I am against adding any code that is only there for testing purposes. However, I make an exception for a simple getter that shows some aspect of the state. In the code above it is the method getPartsHashMapSize() that lets you see how many ongoing collections there are. I know some people frown on this way of exposing internals, but I find it much better and simpler than checking the same thing indirectly. If things change internally, it is trivial to change the method or what it returns. In other words, it is a pragmatic trade-off,  which is a good thing™.

If you haven’t tried this way of structuring time-related code, give it a try. Making sure the code is correct becomes easy, because it is so simple to write the tests.