- Provides java clock which can be used in production code and easily adjusted/changed in tests.
- No passing java
Clock
as a dependency or using mocking libraries is required anymore to test time dependent code. - Have one consistent time provider across the whole application.
- Use
Time
class as a single point of time/clock access in production code.
class SomeService {
void doSomething() {
var now = Time.instance().now(); // or var now = Instant.now(Time.instance().clock());
// do something
}
}
and just alter time in tests using
TestTime.testInstance().setClock(fixedClock);
// or
TestTime.testInstance().fastForward(duration);
// or
TestTime.testInstance().timeFlow(step, endTime, flowSpeedMillis);
TestTime
class allows to alter time in tests without injectingClock
as a dependency.
- Passing a
Clock
as a dependency in production code to make it testableclass SomeService { private final Clock clock; SomeService(Clock clock) { this.clock = clock; } void doSomething() { var now = Instant.now(clock); // do something } }
- if you have good design, and you can easily pass
Clock
as a dependency and test your time dependent code - go for it!
- if you have good design, and you can easily pass
- Update design to pass
Clock
as a dependency (could be challenging in legacy code). - Simulating time flow during scheduled taks. E.g. in Spring Boot tests we can't alter provided fixed
Clock
bean. - Using Mockito - e.g. mocking Instant.now() not working outside of the test scope.
- Inconsistent time across application under test.
- Java 17+
- Java 11 - use timeflow-java11
- Java 8 - use timeflow-java8
<dependency>
<groupId>pl.amazingcode</groupId>
<artifactId>timeflow</artifactId>
<version>1.5.0</version>
</dependency>
implementation group: 'pl.amazingcode', name: 'timeflow', version: "1.5.0"
Instant now = Time.instance().now();
Clock clock = Time.instance().clock();
NOTE: Use
timeflow
library directly in production code. Do not use e.g. as a Spring Bean.
final class Time_Scenarios {
private static final ZoneId ZONE_ID = TimeZone.getTimeZone("Europe/Warsaw").toZoneId();
private static final Clock
FIXED_CLOCK = Clock.fixed(LocalDateTime.of(1983, 10, 23, 9, 15).atZone(ZONE_ID).toInstant(), ZONE_ID);
@AfterEach
void afterEach() {
TestTime.testInstance().resetClock();
}
@Test
void Test_time_jump_for_something() {
// Given
TestTime.testInstance().setClock(FIXED_CLOCK);
var duration = Duration.of(10, ChronoUnit.MINUTES);
// time dependent code under test
// When
TestTime.testInstance().fastForward(duration); // jump forward 10 minutes
// Then
// do assertions
}
@Test
void Test_time_flow_for_something() {
// Given
TestTime.testInstance().setClock(FIXED_CLOCK);
var step = Duration.of(1, ChronoUnit.MINUTES);
var endTime = Time.instance().now().plus(10, ChronoUnit.MINUTES);
var flowSpeedMillis = 100;
// time dependent code under test e.g. scheduled task
// When
TestTime.testInstance().timeFlow(step, endTime, flowSpeedMillis); // simulate speed up time flow with given step
// Then
// do assertions
}
}
- More cases in Time_Scenarios test class.
TestTime.testInstance().registerObserver(clock->System.out.println(clock.instant().toString()));
- Add dependency for ArchUnit
- Detect usage of
TestTime
,Clock
, andnow()
method fromInstant
,LocalDateTime
,LocalDate
,LocalTime
in production code by writing tests:
final class TimeTest {
private static final String ROOT_PACKAGE = "YOUR_APP_ROOT_PACKAGE";
private final JavaClasses classes = new ClassFileImporter()
.withImportOption(new ImportOption.DoNotIncludeTests())
.importPackages(ROOT_PACKAGE);
@Test
void TestTime_not_used_in_production_code() {
noClasses()
.should()
.dependOnClassesThat()
.haveFullyQualifiedName("pl.amazingcode.timeflow.TestTime")
.check(classes);
}
@Test
void Clock_not_used_in_production_code() {
noClasses()
.should()
.dependOnClassesThat()
.haveFullyQualifiedName("java.time.Clock")
.check(classes);
}
@Test
void Instant_now_not_used_in_production_code() {
noClasses()
.that()
.should()
.callMethod(Instant.class.getName(), "now")
.check(classes);
}
@Test
void LocalDateTime_now_not_used_in_production_code() {
noClasses().should().callMethod(LocalDateTime.class.getName(), "now").check(classes);
}
@Test
void LocalDate_now_not_used_in_production_code() {
noClasses().should().callMethod(LocalDate.class.getName(), "now").check(classes);
}
@Test
void LocalTime_now_not_used_in_production_code() {
noClasses().should().callMethod(LocalTime.class.getName(), "now").check(classes);
}
}
- Can't create ticket for past event
- Ticket is expired after event
final class Ticket {
private final Instant eventDateTime;
private Ticket(Instant eventDateTime) {
this.eventDateTime = eventDateTime;
}
public static Ticket create(int year, int month, int dayOfMonth, int hour, int minute, String timezone) {
var zoneId = ZoneId.of(timezone);
var eventDateTime = LocalDateTime.of(year, month, dayOfMonth, hour, minute).atZone(zoneId).toInstant();
var now = Time.instance().now(); // <1>
if (eventDateTime.isBefore(now)) {
throw new IllegalArgumentException("Cannot create ticket for past event!");
}
return new Ticket(eventDateTime);
}
public boolean isExpired() {
var now = Time.instance().now(); // <2>
return eventDateTime.isBefore(now);
}
}
- Use
timeflow
library to obtain current time - Use
timeflow
library to obtain current time
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
final class Ticket_Scenarios {
private static final ZoneId ZONE_ID = TimeZone.getTimeZone("Europe/Warsaw").toZoneId();
private static final Clock FIXED_CLOCK =
Clock.fixed(LocalDateTime.of(2020, 10, 22, 20, 30).atZone(ZONE_ID).toInstant(), ZONE_ID);
@AfterEach
void afterEach() {
TestTime.testInstance().resetClock(); // <5>
}
@Test
void Cant_create_ticket_for_past_event() {
// Given
TestTime.testInstance().setClock(FIXED_CLOCK);
// When
var throwable = catchThrowable(() -> Ticket.create(2019, 1, 17, 20, 30, "Europe/Warsaw"));
// Then
then(throwable)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Cannot create ticket for past event!");
}
@Test
void Ticket_expires_after_event() {
// Given
TestTime.testInstance().setClock(FIXED_CLOCK); // <1>
var ticket = Ticket.create(2020, 10, 23, 20, 30, ZONE_ID.getId()); // <2>
TestTime.testInstance().fastForward(Duration.ofDays(2)); // <3>
// Then
then(ticket.isExpired()).isTrue(); // <4>
}
}
- Set current time to
2020-10-22 20:30
at given timezone. - Create ticket for future event at
2020-10-23 20:30
at given timezone. - Fast forward time by 2 days to
2020-10-24 20:30
at given timezone. - Check ticket is expired.
- Reset clock after each test to default value not to affect other tests.