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 FixedClockConfig {

  @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;

  @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;

  @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!