Pixum Software Engineering Blog
An insight into our development team, what we do and how we work

Testing with dates in Java

Christian Seifert

When working with date and time data we often struggle to create good test cases. A typical reflex is to say “We cannot really test this as we don’t know when the test is run and the object under test might give us different results depending on the day and the time”.

However there are some good strategies to test our code, even when it depends on specific dates and times.

Example

Let’s take an example requirement and walk our way through until we have a valid test suite.

Create a service that is able to create a creating for a customer approaching a store. The service will be used by another application that shows the message on a TV screen in the window of the store.

  • If the customer arrives at the store on the weekend he/she should be greeted with the message “Sorry, we’re closed on weekends”.
  • If the customer arrives during business hours (after 9:00 and before 20:00 hours) he/she should be greeted with the message “Hello, nice to see you!
  • At any time outside of the business hours the customer should be greeted with “Sorry, we’re closed”.

First iteration

Putting these requirements into code, we could end up with the following solution:

public class GreetingService {

    public String createGreeting() {
        LocalDateTime now = LocalDateTime.now();
        if (now.getDayOfWeek().equals(DayOfWeek.SATURDAY) || now.getDayOfWeek().equals(DayOfWeek.SUNDAY)) {
            return "Sorry, we're closed on weekends";
        } else if (now.getHour() >= 9 && now.getHour() <= 19) {
            return "Hello, nice to see you!";
        } else {
            return "Sorry, we're closed";
        }
    }

}

The application showing the message on the TV screen could use this service like this:

GreetingService service = someServiceRegistry.getService(GreetingService.class);
String greeting = service.createGreeting();
tv.showOnScreen(greeting);

A test for the createGreeting method could look like this:

public class GreetingServiceTest {

    @Test
    public void createGreeting() {
        GreetingService underTest = new GreetingService();
        String result = underTest.createGreeting();
        Assertions.assertEquals("Hello, nice to see you!", result);
    }

}

Here we start to run into our main issue: The test will only succeed if it is run during business hours (Monday to Friday, from 9:00 to 20:00 hours).

How do we make sure that the test always succeeds and that the other use cases can be tested as well?

The main issue is the creation of the LocalDateTime object within the GreetingService specifically the call to the LocalDateTime.now() method. As the name suggests, it returns the current time or more specifically the current time of the device where the code is running.

So in order to be independent of the current time we have to somehow be able to set the time explicitly.

Second iteration: Letting the client take care of looking at the clock

Obe way to solve this issue is to move the creation of the LocalDateTime object out of the GreetingService and have the client of the service take care of looking at the clock and pass the current date and time to the service.

A potential client would then need to pass a LocalDateTime object to the GreetingService:

GreetingServiceWithDateAsParameter service = someServiceRegistry.getService(GreetingServiceWithDateAsParameter.class);
String greeting = service.createGreeting(LocalDateTime.now());
tv.showOnScreen(greeting);

The GreetingService itself needs to be modified slightly:

public class GreetingServiceWithDateAsParameter {

    public String createGreeting(LocalDateTime date) {
        if (date.getDayOfWeek().equals(DayOfWeek.SATURDAY) || date.getDayOfWeek().equals(DayOfWeek.SUNDAY)) {
            return "Sorry, we're closed on weekends";
        } else if (date.getHour() >= 9 && date.getHour() <= 19) {
            return "Hello, nice to see you!";
        } else {
            return "Sorry, we're closed";
        }
    }

}

Now we can easily test our service, as we can (and need to) explicitly create the LocalDateTime object before calling the createGreeting method:

public class GreetingServiceWithDateAsParameterTest {

    @Test
    public void shouldBeOpenDuringBusinessHours() {
        GreetingServiceWithDateAsParameter underTest = new GreetingServiceWithDateAsParameter();
        String result = underTest.createGreeting(LocalDateTime.of(2022, 07, 12, 10, 30));
        Assertions.assertEquals("Hello, nice to see you!", result);
    }

    @Test
    public void shouldBeClosedOutsideOfBusinessHours() {
        GreetingServiceWithDateAsParameter underTest = new GreetingServiceWithDateAsParameter();
        String result = underTest.createGreeting(LocalDateTime.of(2022, 07, 12, 22, 00));
        Assertions.assertEquals("Sorry, we're closed", result);
    }

    @Test
    public void shouldBeClosedOnWeekends() {
        GreetingServiceWithDateAsParameter underTest = new GreetingServiceWithDateAsParameter();
        String result = underTest.createGreeting(LocalDateTime.of(2022, 07, 10, 10, 30));
        Assertions.assertEquals("Sorry, we're closed on weekends", result);
    }

}

Third iteration: Configuring the service

The previous solution works pretty well. It allows us to test all the edge cases and returns the same results, no matter when (or where) the test is run.

But why does the client has to be responsible for taking a look at the clock and pass that value to the service? The client doesn’t care about the current time, it only cares about the result given at the current time.

So the original client code is much closer to the way I want the client to behave:

GreetingService service = someServiceRegistry.getService(GreetingService.class);
String greeting = service.createGreeting();
tv.showOnScreen(greeting);

Is there a way to achieve both? Simplicity in the client and testability of the service?

Yes, there is!

We can make the service’s resolution of the LocalDateTime dynamic by using an explicit clock instead of using an implicit clock used by LocalDateTime.now().

Let’s see the final version of our service:

public class GreetingServiceWithClock {

    private Clock clock = Clock.systemDefaultZone();

    public String createGreeting() {
        LocalDateTime date = LocalDateTime.now(this.getClock());
        if (date.getDayOfWeek().equals(DayOfWeek.SATURDAY) || date.getDayOfWeek().equals(DayOfWeek.SUNDAY)) {
            return "Sorry, we're closed on weekends";
        } else if (date.getHour() >= 9 && date.getHour() <= 19) {
            return "Hello, nice to see you!";
        } else {
            return "Sorry, we're closed";
        }
    }

    Clock getClock() {
        return this.clock;
    }
    void setClock(Clock clock) {
        this.clock = clock;
    }

}

By default the service uses the standard system Clock (which is the same one used internally by LocalDateTime.now()).

When writing our unit test we simply need to replace the internal Clock of the service with a mocked Clock that returns exactly the time we want the service to interpret as “now”:

public class GreetingServiceWithClockTest {

    @Test
    public void shouldBeOpenDuringBusinessHours() {
        GreetingServiceWithClock underTest = new GreetingServiceWithClock();
        Instant underTestInstant = LocalDateTime.of(2022, 07, 12, 10, 30).atZone(ZoneId.of("UTC")).toInstant();
        underTest.setClock(Clock.fixed(underTestInstant, ZoneId.of("UTC")));
        String result = underTest.createGreeting();
        Assertions.assertEquals("Hello, nice to see you!", result);
    }

    @Test
    public void shouldBeClosedOutsideOfBusinessHours() {
        GreetingServiceWithClock underTest = new GreetingServiceWithClock();
        Instant underTestInstant = LocalDateTime.of(2022, 07, 12, 22, 00).atZone(ZoneId.of("UTC")).toInstant();
        underTest.setClock(Clock.fixed(underTestInstant, ZoneId.of("UTC")));
        String result = underTest.createGreeting();
        Assertions.assertEquals("Sorry, we're closed", result);
    }

    @Test
    public void shouldBeClosedOnWeekends() {
        GreetingServiceWithClock underTest = new GreetingServiceWithClock();
        Instant underTestInstant = LocalDateTime.of(2022, 07, 10, 10, 30).atZone(ZoneId.of("UTC")).toInstant();
        underTest.setClock(Clock.fixed(underTestInstant, ZoneId.of("UTC")));
        String result = underTest.createGreeting();
        Assertions.assertEquals("Sorry, we're closed on weekends", result);
    }

}

We have achieved both: Simplicity for the client as well as testability of the service itself.

Conclusion

The main challenge for testing logics that rely on date and time values is the fluidity of these values. We can conquer this challenge by allow us to remove this fluidity for certain cases, typically by mocking the clock that is used to get the current date and time.

It requires a bit of thinking and a bit of additional code but is well worth the effort to ensure that all those nasty little edge cases can be tested successfully.