Mocking the java.time API for Better Testability
Date/time logic has many edge cases. Here, we'll look at strategies to bend the clock to your will for better testability.
Join the DZone community and get the full member experience.
Join For FreeDate/times logic in code is where the messiness of the real world upsets the relatively straightforward rules of the digital realm. Blame the bewildering hodgepodge of edge cases on the movement of celestial bodies and pope Gregory XIII (of the Gregorian calendar) but deal with it you must. I’m sure you all know that the 31st of December can be in week 52 or 53, while the 1st of January can be in week 0 or 1 and that you know these rules by heart for various countries (oh yes!). I’m also confident you can hand-code the logic to calculate the difference in seconds between two representations that span multiple time zones as well as a jump in daylight saving time.
Don't Wait for the Next Leap Year
Forgive my sarcasm. Edge cases with time should be tested, especially if your code has homegrown date/time logic (which you should keep to a minimum). Application code deals with time representations that are either passed as one of the java.time classes, or as strings in some serialized format, e.g. 2022-12-03. For this category, you make up enough salient edges and make sure they are covered in unit tests. But often the code needs to know what the current time is. This new, unpredictable timestamp may just end up in a log statement, but it can also be the input to much more business-critical logic. Whenever you reference any of the *.now()
methods, your program has introduced a side effect and become less deterministic.
The problem for traditional unit tests is obvious: We can’t afford to wait for the next leap day to ensure the code handles the 29th of February well. So, we don’t usually test for it and hope all is well. I once witnessed an inexplicable crash that had the whole team puzzled. It disappeared the next day, on the first of March. It turned out just as we expected: a naïve, homegrown Date handling algorithm that would crash every four years.
Four Strategies to Manipulate the Clock
In this tutorial, I will offer some strategies to help ensure that code that asks for the current time can still be properly unit-tested. The approach in all cases is to intercept or replace the default API calls with a call to a mock or test double that returns a date/time configured in the test, rendering our production code deterministic again. Now, the best strategy is not to query the current time in a method when you can also receive it as a parameter. Minimize the places where the code needs to ask the time. There are four basic strategies with which testing code can manipulate the clock.
- Wrap calls to the java.time API
now()
methods into a custom static class - Use an injectable custom DateTime service.
- Inject a subclass of
java.time.Clock
. - Intercept static calls to the
now()
methods with a mocking framework.
The first three strategies are flexible in what you can handle but require some modest changes to the production code. The last option can only handle fixed dates but leaves your production code untouched, which can be a compelling reason to use it.
You can find all code samples in the accompanying GitLab project:
git clone git@gitlab.com:jsprengers/testing-for-time.git
Strategy 1: A Static Wrapper
Let’s look at the simplest variant: a static class that wraps a call to LocalDateTime.now()
public static LocalDateTime currentDateTime() {
return LocalDateTime.now();
}
In your production code, you use it as follows:
public ExecutionResult runBatchWithWrapper() {
LocalDateTime started = DateTimeWrapper.currentDateTime();
simulateLengthyOperation();
LocalDateTime finished = DateTimeWrapper.currentDateTime();
return new ExecutionResult(started, finished);
}
Suppose we want it to always return a fixed instant. In our test, we can configure this as follows:
public class DateTimeWrapper {
private static LocalDateTime instant;
public static void setFixed(Instant instant) {
instant = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
}
public static LocalDateTime currentDateTime() {
if (instant != null) {
return instant;
} else {
return LocalDateTime.now();
}
}
}
After calling setFixed(..)
in your test, you get the same value whenever the current date/time is queried. You have effectively stopped the clock. That may however not always be what you want. Our sample code registers the difference between two invocations to the current time and these should not be the same. We want to move the clock forward for our test, but not have it stop ticking. Easy, we configure it to add a given duration to the current time. Each option invalidates the other. Offsets in the past are not supported here, but I'm sure you can implement that yourself.
public class DateTimeWrapper {
private static LocalDateTime instant;
private static Duration offset;
public static void setFixed(Instant instant) {
DateTimeWrapper.instant = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
offset = null;
}
public static void setOffset(Duration duration) {
offset = duration;
instant = null;
}
public static LocalDateTime currentDateTime() {
if (instant != null) {
return instant;
} else if (offset != null) {
return LocalDateTime.now().plus(offset);
} else {
return LocalDateTime.now();
}
}
}
We can run the test as follows:
@Test
void runCustomDateTimeServiceWithFixedTime() {
DateTimeWrapper.setFixed(fixedInstant);
var result = service.runBatchWithWrapper();
//assertions
}
Strategy 2: Injecting a Custom Mutable DateTimeService
Something feels off with this approach, though. It’s not great that production code has access to these setter methods. There’s a valid point in not introducing testing utilities in non-test code. A better, slightly more involved, strategy is to use dependency injection and have a production version next to a mutable test version. This can look as follows:
We define a simple DateTimeService interface
public interface DateTimeService {
LocalDateTime currentLocalDateTime();
}
With the following production implementation:
@Profile("!test")
@Service
public class DateTimeServiceImpl implements DateTimeService{
public LocalDateTime currentLocalDateTime(){
return LocalDateTime.now();
}
}
And a mutable version for testing:
//The test implementation contains the same logic as the static wrapper
@Profile("test")
@Service
public class MutableDateTimeService implements DateTimeService {
@Override public LocalDateTime currentLocalDateTime() {
[...]
}
}
And we use it as follows:
@Autowired
DateTimeService dateTimeService;
public ExecutionResult runBatchWithCustomDateTimeService() {
LocalDateTime started = dateTimeService.currentLocalDateTime();
simulateLengthyOperation();
LocalDateTime finished = dateTimeService.currentLocalDateTime();
return new ExecutionResult(started, finished);
}
We have two implementations of the DateTimeService
and only one of them is instantiated. Since the DateTimeServiceIntegrationTest
runs under the ‘test’ profile, it picks the MutableDateTimeService
instance. In production, Spring picks the default DateTimeServiceImpl
.
@SpringBootTest()
@Import(MutableDateTimeService.class)
@ActiveProfiles("test")
public class DateTimeServiceIntegrationTest {
}
Strategy 3: Injecting a java.time.Clock Instance
A similar injection-based approach that does not require you to write and configure a boilerplate production implementation uses the java.time.Clock
class. The java.time API has an abstract class Clock that you can add as an argument to all the now()
methods. By picking a different Clock, you can pretend that you’re in Beijing or that it’s forever 29 February 2016. In production, all that is needed is to inject a Clock instance and use it as follows:
@Autowired
Clock clock;
public ExecutionResult runBatchWithClock() {
LocalDateTime started = LocalDateTime.now(clock);
}
Unless otherwise configured, Spring picks java.time.Clock.SystemClock
. If, in your test, you want to use a fixed date or an offset, you can use Clock.fixed(..)
or Clock.offset(..)
respectively in a Spring configuration class:
@Configuration
public class TestConfig {
@Bean
Clock fixedClock(){
return Clock.fixed(Instant.from(someDateTime), ZoneId.systemDefault());}
}
For test purposes, the standard implementations fall short if you want to manipulate your offset or fixed time dynamically because the fixed instants or offsets in Clock
objects are immutable after construction. A likely use case would be to simulate thirty runs of a scheduled daily batch process, advancing the clock by 24 hours each time. But Clock
is not final, so we can make our own MutableClock
version. It's very similar to all the other classes. Have a look at it in the gitlab project.
Strategy 4: Intercepting Static Invocations With Mockito
The last mocking method is an altogether different beast. It does not need dependency injection and leaves your production code entirely untouched. It comes down to intercepting the static method calls to the time API and configuring the responses with the help of the Mockito mocking framework. Mocking static methods is no longer the hassle it once was. It comes standard with Mockito since 3.4.0 but you need to add the extra Mockito-inline dependency (see the pom.xml
in the project).
@Test
void runMockedDateTimeWithFixedTime() {
try (MockedStatic<LocalDateTime> mockedStatic = Mockito.mockStatic(LocalDateTime.class)) {
mockedStatic.when(() -> LocalDateTime.now(ArgumentMatchers.any(Clock.class))).thenReturn(fixedLocalDateTime);
var result = service.runBatchWithClock();
assertThat(result.started()).isEqualTo(result.finished());
}
//notice that the static mocking is only in effect within the above try block, which is of course how we would want it.
assertThat(LocalDateTime.now().getYear()).isGreaterThanOrEqualTo(2022);
}
Final Advice: Don't DIY
I hope you have found this tutorial useful. Let me close with a final word of advice, based on my experience with some clueless DIY date handling by inexperienced developers. The most compelling arguments for not re-inventing the wheel are the same reasons why you should never roll your own cryptography solution. Firstly, it’s harder than you think. The best wheel you can re-invent will still be unprepared for the bumpy road of unexpected edge cases. But more importantly, it’s a highly common requirement, so the generic solutions already handle almost everything you can possibly need. This goes also for any test utilities that I presented in this tutorial. Keep it simple.
Opinions expressed by DZone contributors are their own.
Comments