Chapter 6. Things You Should Know
6.11. Time is not on Your Side
Time is making fools of us again.
18http://tempusfugitlibrary.org
Chapter 6. Things You Should Know
— J.K. Rowling Harry Potter and the Half-Blood Prince (2005)
Alas, after all the years of software development we still cannot get it right! The number of bugs related to time formatting and storage is horrifying. Wikipedia gives a lot of examples of such issues, with the famous Y2K problem among them19. There is definitely something complicated connected with time – something which makes us write code not in conformity with its quirky and unpredictable nature. In this section we will see how to deal with classes whose behaviour is determined by time.
A typical example of a time-dependent class is shown in Listing 6.34. The code itself is trivial, but testing such code is not.
Listing 6.34. Time-dependent code
public class Hello {
public String sayHello() {
Calendar current = Calendar.getInstance();
if (current.get(Calendar.HOUR_OF_DAY) < 12) { return "Good Morning!";
} else {
return "Good Afternoon!";
} } }
Returns the current date (at the time of the test’s execution).
Please do not be offended by this "HelloWorld" style example. Its point is that it encapsulates everything problematic about the unit testing of time-dependent code. I have seen complicated business code with exactly the same issue as is shown in Listing 6.34. If you learn to solve the problem of Hello class, you will also know how to deal with much more complicated logic.
Whatever test we write in connection with this simple Hello class, its result will depend on the time of execution. An example is shown in Listing 6.35. After execution, one of the tests will fail. This is something we cannot accept. What is expected of unit tests is that they abstract from the environment and make sure that a given class always works.
Listing 6.35. Time-dependent code - a failing test
public class HelloTest { @Test
public void shouldSayGoodMorningInTheMorning() { Hello hello = new Hello();
assertEquals("Good Morning!", hello.sayHello());
} @Test
public void shouldSayGoodAfternoonInTheAfternoon() {
Chapter 6. Things You Should Know One of these assertions will fail.
We need to think of something different… And voila! The trick is to make time a collaborator of the
Hello class. In order to do this, we need to:
• create a new interface - see Listing 6.36,
• redesign Hello class a little bit - see Listing 6.37.
Listing 6.36. The TimeProvider interface
/**
* Allows for taking control over time in unit tests.
*/
public interface TimeProvider { Calendar getTime();
}
A default implementation of the TimeProvider interface shown in Listing 6.36 would probably reuse the system calendar (Calendar.getInstance()).
Listing 6.37. Time as a collaborator
public class HelloRedesigned {
private TimeProvider timeProvider;
public HelloRedesigned(TimeProvider timeProvider) { this.timeProvider = timeProvider;
}
public String sayHello() {
Calendar current = timeProvider.getTime();
if (current.get(Calendar.HOUR_OF_DAY) < 12) { return "Good Morning!";
} else {
return "Good Afternoon!";
} } }
The TimeProvider collaborator has been injected as a constructor parameter, which means it is easily replaceable with a test double.
timeProvider.getTime() is used instead of Calendar.getInstance().
An inquisitive reader may notice that the design of our Hello class can still be improved by moving more functionality into the TimeProvider class. We will address this in one of the exercises at the end of this chapter (see Section 6.15).
Suddenly, the tests of the redesigned Hello class have become trivial.
Chapter 6. Things You Should Know
Listing 6.38. Testing time - setting the stage
public class HelloRedesignedTest { private HelloRedesigned hello;
private TimeProvider timeProvider;
@Before
public void setUp() {
timeProvider = mock(TimeProvider.class);
hello = new HelloRedesigned(timeProvider);
} ...
}
Here a mock of TimeProvider has been created and injected into the SUT20.
Listing 6.39. Testing time - morning
...
private static final Object[] morningHours() { return $(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
} @Test
@Parameters(method = "morningHours")
public void shouldSayGoodMorningInTheMorning(int morningHour) { when(timeProvider.getTime())
.thenReturn(getCalendar(morningHour));
assertEquals("Good Morning!", hello.sayHello());
}
Test methods takes parameters provided by data provider.
Here we have stubbing of the timeProvider test double.
Listing 6.40. Testing time - afternoon
...
private static final Object[] afternoonHours() {
return $(12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23);
} @Test
@Parameters(method = "afternoonHours")
public void shouldSayGoodAfternoonInTheAfternoon(int afternoonHour) { when(timeProvider.getTime())
.thenReturn(getCalendar(afternoonHour));
assertEquals("Good Afternoon!", hello.sayHello());
}
private Calendar getCalendar(int hour) {
Chapter 6. Things You Should Know Test methods takes parameters provided by data provider.
Here we have stubbing of the timeProvider test double.
If you run this new test it should pass. No matter if it is 3 am or 6 pm, it will pass. Apparently, time is not a issue anymore. :)
Redesign Is Not The Only Way. This redesign trick (i.e. implementing time provider as a separate entity) is not the only way of dealing with such time-dependent code. Using tools like PowerMock or JMockit it is possible to convince the Calendar class to return whatever value we want. I do not recommend such solutions, because they do not make the SUT’s code any better. Instead they make its current implementation fixed, by repeating some of its implementation details within the test code (see Section 7.6 for a discussion of the trade-offs between the redesign option and its alternatives).
Nevertheless, I do believe you should know about the different options – which is why I mention them.
6.11.1. Test Every Date (Within Reason)
Now that we know how to deal with time-dependent code - it really is that simple! - it is time for some general, but nevertheless very important, remarks.
Numerous bugs relate to undertested code, which depends on time. Developers seem to share a naive faith that "if it works today, then it should also work tomorrow". Well, not necessarily. The point I want to make is that you should be really, really cautious when dealing with time, which in essence means the following:
• be pedantic when writing unit tests for such code,
• run your tests with at least a few subsequent years in mind.
With unit tests, there really is no excuse for not running tests for all possible dates or hours. Why not run tests for 5 years in advance, if your tests are ultra-fast? Just do it and then you can rest confident that for the next five years you will not encounter any time-related issue in that particular piece of code.
6.11.2. Conclusions
In this section we have discussed how to deal with time-dependent SUTs. The solution is quite simple:
make time one of the SUT’s collaborators, and then do exactly what you would with other collaborators:
replace it with a test double. Once you have done this, your design will be improved (the SUT is freed from time-management knowledge and can focus on business logic), and you can then proceed with a standard approach to all collaborators. Yes, this is enough to triumph over time (at least in unit tests). :) The technique presented in this section is sufficient to deal with the time problem at the unit-testing level. However, it does not solve many problems related to time that you will encounter when testing applications on a higher level. The different time-zones of servers and databases, triggers in databases, or external processes running only during certain hours of the night, as well as security certificates whose viability expires, will all give you a good deal of trouble. Fortunately, though, none of these affect unit tests!
Chapter 6. Things You Should Know