How to Test Date and Time in Spring Boot
Testing temporal events in Spring Boot Java applications with JUnit 5 and Spring Boot Test.
Last week, on February 1st, our build pipeline noticed a failing test. It showed up that this test, written several weeks ago, in January, passed indeed only in January.
January is the only month that string-concatenated with "1"
gives you a valid month — "11"
, November. Starting with 1st February, the test made no more sense as "21"
is obviously no valid month.
Albeit it was quite easy to fix this “everwinter” bug, the situation demonstrates well how tricky testing time could be. If your tests ever failed when close to midnight, in a leap year, or on a machine in a different time zone, then you know what I am talking about.
Test in isolation. I guess you have already heard this statement. It is often understood as isolating units under test from each other. This explanation caused the explosion of mock-based testing. I believe it completely misses the point.
The tests should be isolated from each other, so they are independent of execution order and side-effects. Tests should also be isolated from environment specifics, independent of volatile and unstable data.
What can be more volatile than ever-in-flux, elusive time? We need to find a way how to control time in our tests.
Testing Date and Time
Consider a simple example of placing a new Order:
@Service class OrderService { public Order place(String content) { return Order.builder() .id(UUID.randomUUID()) .createdAt(ZonedDateTime.now()) .content(content) .build(); } }
How can we test that the Order was created at the current time?
@Test void order_is_placed_at_current_time() { var order = orderService.place("Test"); assertThat(order.getCreatedAt()) .isEqualTo(ZonedDateTime.now()); }
Nope, this won’t work. There are tools to help us with this tedious task:
assertThat(order.getCreatedAt()) .isCloseTo(ZonedDateTime.now(), within(1, ChronoUnit.SECONDS));
Yes, this will work. But what if the placing takes more than one second? What if even nanoseconds matter?
Sometimes, we need a solution that brings us one hundred percent certainty. Sometimes, we just need to control the clock.
Mocking the Clock
An obvious option is mocking. We already mock almost everything (sadly), so why not mock time as well?
Date and time in Java can be calculated dependently on a java.time.Clock
instance:
var clock = Clock.systemUTC(); var zdt = ZonedDateTime.now(clock);
We can use this tactic for our advance:
@Service class OrderService { private final Clock clock = Clock.systemUTC(); public Order place(String content) { return Order.builder() .id(UUID.randomUUID()) .createdAt(ZonedDateTime.now(clock)) .content(content) .build(); } }
When using the Mockito inline maker we can easily mock static methods:
static Clock clock; @BeforeAll static void setupClock() { clock = Clock.fixed( Instant.parse("2020-12-01T10:05:23.653Z"), ZoneId.of("Europe/Prague")); Mockito.mockStatic(Clock.class) .when(Clock::systemUTC).thenReturn(clock); } @Test void order_is_placed_at_current_time() { var order = orderService.place("Test"); assertThat(order.getCreatedAt()) .isEqualTo(ZonedDateTime.now(clock)); }
This will work, but there is a lot of boilerplate code and an amount of black magic going on.
Mocking static methods will definitely impact other tests that use the methods which means that just lost test isolation again. Can we do better?
Fixing the Clock
Why not just use the basic Spring feature and inject the clock as a bean dependency?
@Service class OrderService { private final Clock clock; public OrderService(Clock clock) { this.clock = clock; } ... }
Now, when the clock became a dependency of the service, we have to provide a default instance to the Spring context:
@SpringBootApplication public class ShoppingApplication { public static void main(String[] args) { SpringApplication.run(ShoppingApplication.class, args); } @Bean Clock clock() { return Clock.systemUTC(); } }
We can change this instance arbitrarily, like providing a fixed clock for our tests:
@TestConfiguration class ClockConfig { @Primary @Bean Clock fixedClock() { return Clock.fixed( Instant.parse("2020-12-01T10:05:23.653Z"), ZoneId.of("Europe/Prague")); } } @SpringBootTest(classes = FixedClockConfig.class) class OrderTests { @Autowired private Clock clock; @Autowired private OrderService orderService; @Test void order_is_placed_at_current_time() { var order = orderService.place("Test"); assertThat(order.getCreatedAt()) .isEqualTo(ZonedDateTime.now(clock)); } }
We can use the same test configuration in multiple tests:
@SpringBootTest(classes = ClockConfig.class) class OrderTests { ... } @SpringBootTest(classes = ClockConfig.class) class DeliveryTests { ... }
Controlling the Clock
When a fixed clock is not enough, we can gain more control by mixing both worlds: injecting a mocked clock:
@SpringBootTest class OrderTests { @MockBean private Clock clock; @Autowired private OrderService orderService; @BeforeEach void setupClock() { when(clock.getZone()).thenReturn( ZoneId.of("Europe/Prague")); } @Test void order_is_placed_at_current_time() { when(clock.instant()).thenReturn( Instant.parse("2020-12-01T10:05:23.653Z")); var order = orderService.place("Test"); assertThat(order.getCreatedAt()) .isEqualTo(ZonedDateTime.now(clock)); } }
Summary
We have seen how to test date and time with Spring Boot and Mockito.
Controlling time brings certainty into tests, makes them isolated from the environment, independent, and stable.
The source code can be found on my GitHub.
Happy timing!