Programming in Java: How to automatically test time-controlled features

in #programming5 years ago (edited)

In this short programming-related article let's consider a Java application where some logic is time-dependent. Examples of such can be:

  • Communication - where a certain action has to be taken after timeout.
  • Cache - where items should expire after given time.
  • Defense barriers - where after specific amount of things in time unit happens it triggers some actions.
  • Process handler - where an external process should respond in limited time or we take actions to kill/recover it.

Obviously there can be many other cases. With knowledge of multi-threading basics it shouldn't be too hard to write the logic but to write automatic tests for it... well that's different thing... and most of developers just don't write such tests due to difficulty it brings.

The simplest solution is to write a test that actually performs a wait and checks what happens. This is only doable if the waiting period isn't too long. If it's configurable then test suite may have it set up for single second and count that all the logic will manage to be faster than a second and only the one that explicitly waits will get caught. If it's not configurable... it's getting worse (but there are only limited reasons why not make such wait conditions configurable in properties or anywhere else). Waiting minutes to perform a test may not always be acceptable especially when tests are running in large batches very often. This may slow down the whole test suite to run for many hours.

The other simple solution is that the test has been given a power to change system time to skip waiting and make tests faster. This is very bad idea for at least three reasons:

  • System time is for all applications and may ruin other tests or applications. It will also ruin time in logs etc. This is very dirty solution.
  • You may need to remember to always set the time back to proper hour even if the tests fail in unexpected way.
  • Some Java mechanisms are time-swithing-proof. You may end up with no results at all if waiting methods used CPU tick counts, system up-time or other independent measures (like System.nanoTime).

Another solution is to make the logic a little test-aware. This is a little messy but in most cases acceptable. In fact it's a most popular solution right now. If you want to make something testable make a component out of it which you will be able to replace with mock. But how to mock and rewind time? Well you can write something which we may call a time service. A component that is somewhere in a scope of your code and is being commonly used just to tell the time. Once you have it all testable code should use it. Instead of taking time from system methods or direct creation of dates/instances always ask the time service what time it is. It is somewhat burdensome, though. You will need to write all those utility methods to return milliseconds, instances, dates, whatever. Why nobody never added it to Java itself? Well it is already there (since Java 8) and it's called a Clock.

Using Clocks in your application

You can simply use java.time.Clock class to get the current system time in most of your production logic. It has the ability not only to give you the current time (as a UTC milliseconds long or as Instant) but also to give you another clocks that answers with specific offset to the current one or just the frozen ones (simple mocks). The usage goes as follows:

Clock systemClock = Clock.systemDefaultZone(); // Clock in system time zone
Clock utcClock = Clock.systemUTC(); // This clock ticks in UTC
Clock fixedClock = Clock.fixed(Instant.parse("2018-04-29T10:15:30.00Z"), ZoneId.of("Asia/Calcutta")); // This clock does not tick, it's mocked frozen time
  
Clock tomorrowClock = Clock.offset(systemClock, Duration.ofDays(1)); // This clock will tick 1 day ahead of system time
Instant tomorrow = tomorrowClock.instant(); // Instant tomorrow from now
 
LocalDateTime dateAndTime = LocalDateTime.now(systemClock); // This LocalDateTime is equivalent of normal new LocalDateTime instance but uses clock (testability!)

Once you use clocks you can make them replaceable in your classes by accepting them in constructor:

class MyClaz {
  private final Clock clock;
 
  public MyClaz(Clock clock) {
    this.clock = clock;
  }
 
  // Methods...
}

If you use Spring you can add a clock instance as a normal Spring bean in your configuration class. This way unless you override the Spring configuration for some of your beans you will use this default one.

@Configuration
public class SpringConfig {

  @Bean
  public Clock systemClock() {
    return Clock.systemDefaultZone();
  }
  
}

This will allow you to simply use clocks in constructors of your beans (Spring will autowire it):

@Component
class MyClaz {
  private final Clock clock;
 
  @Autowired
  public MyClaz(Clock clock) { // It's defined in my SpringConfig
    this.clock = clock;
  }
 
  // Methods...
}

This way the test may look like this (assuming you use Mockito):

public TestClaz {
 
  @Mock
  private Clock mockedClock;
 
  @Test
  public void timeoutNeverHappens() {
    Clock staticClock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
    MyClaz testSubject = new MyClaz(staticClock);
    // we perform tests here and not worry about the timeouts as the time is frozen
  }
 
  @Test
  public void timeoutHappens() {
    final Instant start = Instant.now();
    final int timeoutSeconds = 5;
 
    doAnswer(invocationOnMock -> {
            if (someCondition()) {
                return start; // Initial time returned before some condition happens
            } else {
                return start.plusSeconds(timeoutSeconds + 1L); // Timeout after the condition happened
            }
        }).when(mockedClock).instant();
 
    MyClaz testSubject = new MyClaz(mockedClock);
    // we perform tests here and expect timeout once we switch method someCondition() to return true
  }
}

Obviously there are always some limitations. Using clocks is not always advisable, use your technical skills to determine where not to use them by yourself. Examples of such cases may be:

  • Just to print debug or log something.
  • When you measure something's duration in test logic itself (there will be no point to test a test... right?).
  • When you relay on external library/solution that won't make use of a clock anyway.

I hope that after reading this you won't be afraid of writing tests for time-related features as those are just another, normal features of your application and should be tested like everything else.

Happy testing until next time!