Thursday, 23 September 2010

Unit-Testing Time-Oriented Code

Part 2: Are We There Yet?

The counterpoint to actively forcing the current thread to wait is checking whether a certain amount of time has elapsed. Unit-testing code like this is a similar challenge; we certainly don't want to have to sleep() in our unit tests in order to verify our code - we have better things to do with our (and our processor's!) time.


Let's look at a typical (if contrived) bit of time-elapsed-measuring code:


  public void sendRequest(Packet packet, int slowThresholdMillis) 
                throws ExcessiveLatencyException {
    final Date sendTime = new Date();
    networkInterface.sendAsynchronously(packet, new AckCallback() {
      public void ackReceived() {
        Date ackTime = new Date();
        long timeTaken = ackTime.getTime() - sendTime.getTime();
        if (timeTaken > slowThresholdMillis) {
          throw new ExcessiveLatencyException(timeTaken);
        }
      }
    }
  }

Now clearly we need to be able to cover that "took too long to respond" path in our unit tests. Of course we could pass in a negative value of slowThresholdMillis but that's not really a realistic situation, and indeed a requirement will probably come along to validate against non-negative and non-zero values for that parameter.


Again the solution is to refactor out the parts which are currently hard to unit test. As a side-effect we'll probably get a much cleaner sendRequest() method - I've certainly never liked seeing all that millisecond-twiddling sitting alongside higher-level abstractions like networkInterface.sendAsynchronously()!

What we need is commonly known as a Stopwatch component, and given there are several proven implementations to choose from, there's absolutely no need to re-invent the wheel here.


Let's go with the Spring implementation because we've almost certainly already got it in our classpath:


public class RequestSender {
  private static final StopWatch DEFAULT_STOPWATCH = new StopWatch();
  private final StopWatch sw;
  
   public RequestSender() {
    this.sw = DEFAULT_STOPWATCH;
  }

  /** Package-visible for unit testing */
  RequestSender(StopWatch stopWatch) {
    this.sw = stopWatch;
  }

  public void sendRequest(Packet packet, int slowThresholdMillis) 
                throws ExcessiveLatencyException {
    sw.start();
    networkInterface.sendAsynchronously(packet, new AckCallback() {
      public void ackReceived() {
        sw.stop();
        if (sw.getLastTaskTimeMillis() > slowThresholdMillis) {
          throw new ExcessiveLatencyException(sw.getLastTaskTimeMillis());
        }
      }
    }
  }

Notice how a minor refactor for testability has already improved the code - we've saved a line and delegated off some drudgery to a reliable third-party library. I've gone for a constructor-injected approach here for two reasons, namely that having a StopWatch is essential to the correct operation of the class - and I would say the same for the (elided) networkInterface collaborator, and secondly, it gives a straightforward way to ensure that sw is final so I can use it in the ackReceived() callback. All that remains now is to mock the StopWatch implementation in our unit tests so we can exercise all the paths through the code:


public class RequestSenderTest {
  private RequestSender testInstance;
  @Mock
  private StopWatch mockStopWatch;

  @BeforeMethod(groups="unit")
  public void setup() {
    MockitoAnnotations.initMocks(this);
    testInstance = new RequestSender(mockStopWatch);
  }

  @Test(groups="unit", expectedExceptions=ExcessiveLatencyException.class)
  public void checkLargeLatencyThrowsException() {
    Mockito.when(mockStopWatch.getLastTaskTimeMillis()).thenReturn(200);
    testInstance.sendRequest(mockPacket, 100);
  }
}

No comments:

Post a Comment

Comments welcome - spam is not. Spam will be detected, deleted and the source IP blocked.