Part 1: Sleepy Time
Every now and again, we come across a need to write code that is time-sensitive. It could be as simple as waiting a certain period before timing-out a request, putting a pre-delay in an autocomplete control, sending a rapid-fire chatter of calls, or measuring performance of some other aspect of the system. In all these cases, unit testing can be a real pain.
We all want our unit tests to run as quickly as possible; if it takes longer than a couple of tens of seconds to run, we'll potentially lose focus, switch to browsing the web and get distracted, or worse yet, not run the tests at all. But if the unit tests for our time-sensitive classes take real-time amounts of time to run, we could be looking at enormously time-consuming test runs (imagine running a suite of 10 or more tests where "timeout after 30 seconds" is the expected outcome!)
The solution is of course to somehow mock out the code that requires or takes time to execute. In effect, we want a mock that lets us "fast-forward" time. Let's first look at the classic naïve way to "wait a bit" - sleep:
public void sendPings(int numberOfPings, int delayMillis) {
for (int i=0; i<numberOfPings; i++) {
sendPing();
waitForMillis(delayMillis);
}
}
private void waitForMillis(int millisToWait) {
try {
Thread.sleep(millisToWait);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
How can we fully test the sendPings() method without incurring the time cost of waiting between pings? Well, we could of course only test with very small values of delayMillis, but that's still going to take a bit of time. Let's look at our overall aim for unit-testing sendPings:
- We'd like to mock out the actual pinging mechanism so we can count how many times it was invoked - hence checking our for-loop logic
- We'd like to verify that the sleeping mechanism is correctly told to sleep for delayMillis
- We can pretty-much trust that the Java sleep() method works as advertised
- We don't want to actually wait numberOfPings times delayMillis milliseconds in order to see if a certain combination works
What we need to do is classic Refactoring, classic Clean Code, classic SRP, classic Design Patterns, classic OO, you name it. We extract the waiting mechanism into a class that is solely concerned with waiting:
public interface WaitProvider { void waitForMillis(int millisToWait); } public class ThreadSleepingWaitProvider implements WaitProvider { public void waitForMillis(int millisToWait) { try { Thread.sleep(millisToWait); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }
So now the interesting bits of our pinging class under test look something like this:
public class PingSender { private static final WaitProvider DEFAULT_WAIT_PROVIDER = new ThreadSleepingWaitProvider(); private WaitProvider waitProvider = DEFAULT_WAIT_PROVIDER; public void sendPings(int numberOfPings, int delayMillis) { for (int i=0; i<numberOfPings; i++) { sendPing(); waitProvider.waitForMillis(delayMillis); } } void setWaitProvider(WaitProvider newWP) { this.waitProvider = newWP; } }
Thus, our default behaviour is still to use the Thread.sleep() mechanism, but we can substitute a mock WaitProvider to speed up our unit tests. We just use the package-visible setter:
public class PingSenderTest { private PingSender testInstance; @Mock private WaitProvider mockWaitProvider; @BeforeMethod(groups="unit") public void setup() { testInstance = new PingSender(); MockitoAnnotations.initMocks(this); testInstance.setWaitProvider(mockWaitProvider); } @Test(groups="unit") public void checkDelayIsPropagatedToWaitProvider() { testInstance.sendPings(3, 333); Mockito.verify(mockWaitProvider, Mockito.times(3)).waitForMillis(333); } }
And so on and so on ... Note that I am not suggesting for a minute that the unit tests using the mock WaitProvider are the only testing that should be done on PingSender. There should be a full set of integration tests which use the "default" provider with reasonable (if short) delays specified, and possibly even a further set (perhaps annotated appropriately) that use relatively large delayMillis values.
In the next installment, we'll look at unit testing the situation where rather than waiting for a set time, we need to test whether a certain amount of time has elapsed.
No comments:
Post a Comment
Comments welcome - spam is not. Spam will be detected, deleted and the source IP blocked.