You are currently viewing Java & SpringBoot: Overriding system time for Unit, Integration and End2End testing – Complete guide

Java & SpringBoot: Overriding system time for Unit, Integration and End2End testing – Complete guide

Have you ever wondering how to make your application travel in time? If you have, let me tell you that’s great because in the following tutorial I’m going to show you in the real examples how to manage time for all of three test layers – unit tests, integration tests and end-to-end testing.

Resources

Watch the tutorial on YouTube

Overview

Let’s think for a moment how we can test the behaviour of our application in different points of time.

The most obvious way is just to wait for time to pass by. But it’s not optimal solution and I think that I don’t have to explain it to you why.

Another solution is targeted to applications that you don’t have control over. It requires to modify bytecode using AspectJ in the way that all of getting time invocations return custom point in time, but it’s not covered in this tutorial.

The last way is targeted to applications that you have full control over. It’s to replace all of the getting time invocations (like LocalDateTime.now(), new Date() … etc) by invoking a custom DateTime provider. It’s very simple solution but it’s easy to implement and it works perfect.

Sample application

For the tutorial purposes I prepared simple SpringBoot application consists of TimeController exposed on the localhost:8080/time REST endpoint. It returns current time from BigBen bean which is responsible for returning current time in the application. 

package com.bettercoding.timetraveling.timetravelingdemo;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.ZonedDateTime;

@RestController
@RequestMapping("/time")
@RequiredArgsConstructor
public class TimeController {
    private final BigBen bigBen;

    @GetMapping("/")
    public ZonedDateTime getCurrentTime() {
        return bigBen.getApplicationTime();
    }
}
package com.bettercoding.timetraveling.timetravelingdemo;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.ZonedDateTime;

@Component
@RequiredArgsConstructor
public class BigBen {
    public ZonedDateTime getApplicationTime() {
        return ZonedDateTime.now();
    }
}
package com.bettercoding.timetraveling.timetravelingdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TimeTravelingDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(TimeTravelingDemoApplication.class, args);
	}

}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.bettercoding.timetraveling</groupId>
	<artifactId>time-traveling-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>time-traveling-demo</name>
	<description>Demo project of time traveling Spring Boot application</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

DateTimeProvider implementation

I decided to implement the following DateTImeProvider as a thread-safe Singleton. The easiest way to achieve it it to use enum class. DateTimeProvider consists of the internal clock, and following methods: 

  • timeNow() – returns current time, based on internal clock
  • setTime() – sets the internal clock as fix time or custom clock passed as parameter
  • resetTime() – resets the internal clock to the default one.
package com.bettercoding.timetraveling.timetravelingdemo;

import java.time.Clock;
import java.time.ZonedDateTime;

public enum DateTimeProvider {
    INSTANCE;

    public static DateTimeProvider getInstance() {
        return INSTANCE;
    }

    private final Clock defaultClock = Clock.systemDefaultZone();
    private Clock clock = defaultClock;

    public ZonedDateTime timeNow() {
        return ZonedDateTime.now(clock);
    }

    public void setTime(ZonedDateTime zonedDateTime) {
        var customClock = Clock.fixed(zonedDateTime.toInstant(), zonedDateTime.getZone());
        this.setTime(customClock);
    }

    public void setTime(Clock clock) {
        this.clock = clock;
    }

    public void resetTime() {
        this.clock = this.defaultClock;
    }

}

What we have to do now is to replace all of getting time invocations(LocalDateTime, new Date(), etc…) by invoking DateTimeProvider.getInstance().timeNow(). So let’s modify BigBen class to use the implemented DateTimeProvider.

package com.bettercoding.timetraveling.timetravelingdemo;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.ZonedDateTime;

@Component
@RequiredArgsConstructor
public class BigBen {

    public ZonedDateTime getApplicationTime() {
        return DateTimeProvider.getInstance().timeNow();
    }
}

Changing time in Unit Tests

Managing the time in UnitTest is very simple, the only thing you have to do is just to invoke DateTimeProvider.getInstance().setTime() before unit test. It’s very important to resetTime() after execution of unit tests because modified time may affect on the other unit tests. Please remember that you cannot run time traveling tests in parallel, because time set in one thread may be overridden by another thread.

package com.bettercoding.timetraveling.timetravelingdemo;


import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class BigBenTimeTravelingUnitTest {

    @BeforeAll
    static void setup() {
        ZonedDateTime customAppTime = ZonedDateTime.parse("1990-01-02T11:00:56+02:00", DateTimeFormatter.ISO_ZONED_DATE_TIME);
        DateTimeProvider.getInstance().setTime(customAppTime);
    }

    @AfterAll
    static void cleanup() {
        DateTimeProvider.getInstance().resetTime();
    }

    @Test
    void timeTravelingTest() {
        //given
        ZonedDateTime expectedTime = ZonedDateTime.parse("1990-01-02T11:00:56+02:00", DateTimeFormatter.ISO_ZONED_DATE_TIME);
        BigBen bigBen = new BigBen();
        //when
        var applicationTime = bigBen.getApplicationTime();
        //then
        Assertions.assertEquals(expectedTime, applicationTime);
    }
}

Managing the time in Integration Tests

Let’s modify BigBen class to use DateTimeProvider bean instead of invoking DateTimeProvider.getInstance().timeNow() directly. This step is not necessary, but I would like to show you, that you can do it too. 

package com.bettercoding.timetraveling.timetravelingdemo;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.ZonedDateTime;

@Component
@RequiredArgsConstructor
public class BigBen {
    private final DateTimeProvider dateTimeProvider;

    public ZonedDateTime getApplicationTime() {
        return dateTimeProvider.timeNow();
    }
}

Before you you run the application you have to provide DateTimeProvider bean configuration. Please, don’t mark DateTimeProvider as a component. It’s very important to create a custom bean configuration that you return DateTimeProvider.getInstance() instead creating new instance.

package com.bettercoding.timetraveling.timetravelingdemo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TimeConfiguration {
    @Bean
    DateTimeProvider dateTimeProvider() {
        return DateTimeProvider.getInstance();
    }
}

If you made all of the previous steps you can modify the time in the similar way as you made it in the unit tests case.

package com.bettercoding.timetraveling.timetravelingdemo;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

@SpringBootTest
class TimeTravelingDemoApplicationTests {
	@Autowired
	private BigBen bigBen;

	@BeforeAll
	static void setup() {
		ZonedDateTime customAppTime = ZonedDateTime.parse("1990-01-02T11:00:56+02:00", DateTimeFormatter.ISO_ZONED_DATE_TIME);
		DateTimeProvider.getInstance().setTime(customAppTime);
	}

	@AfterAll
	static void cleanup() {
		DateTimeProvider.getInstance().resetTime();
	}

	@Test
	void contextLoads() {
	}

	@Test
	void timeTravelTest() {
		//given
		ZonedDateTime expectedTime = ZonedDateTime.parse("1990-01-02T11:00:56+02:00", DateTimeFormatter.ISO_ZONED_DATE_TIME);
		//when
		ZonedDateTime applicationTime = bigBen.getApplicationTime();
		//then
		Assertions.assertEquals(expectedTime, applicationTime);
	}

}

Managing time in End2End testing

End2End testing is usually performed in some test environment that our application exists in and it is managed by some external application (for example TestNg and Selenium based application). Let’s call it End2EndTestPlayer. The application is responsible for playing a test scenario across tested applications. 

End2EndTestPlayer plays some test scenario across multiple applications

The easiest way to test our application in such environment is to expose some REST endpoint that you can set and reset previously implemented DateDimeProvider externally. The following TimeManagementController exposes POST method on /time-management/ resource that allows you to set the time in the application and it also exposes DELETE method for resetting the time. 

It’s not a good idea to always expose the TimeManagementController (for example in production environment) so you can add @ConditionalOnPropoerty annotation and use time-management.enable property to manage environments where the changing time feature should be enabled.

package com.bettercoding.timetraveling.timetravelingdemo;

import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.*;

import java.time.ZonedDateTime;

@RestController
@RequestMapping("/time-management")
@ConditionalOnProperty(prefix = "time-management", name = "enabled", havingValue = "true")
@RequiredArgsConstructor
public class TimeManagementController {
    private final DateTimeProvider dateTimeProvider;

    @PostMapping("/")
    public void changeApplicationTime(@RequestBody ChangeApplicationTimeRequest changeApplicationTimeRequest) {
        this.dateTimeProvider.setTime(changeApplicationTimeRequest.getZonedDateTime());
    }

    @DeleteMapping
    public void resetApplicationTime() {
        this.dateTimeProvider.resetTime();
    }

    @Data
    private static class ChangeApplicationTimeRequest {
        ZonedDateTime zonedDateTime;
    }
}
time-management:
  enabled: true

That’s all what I’ve prepared for you in this tutorial, if I helped you, please consider sharing this post to help me gain a wider audience.
Thanks and I hope to see you in my next tutorial.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments