For my Java Spring test, I have decided to fake the system clock in order to make testing easier
I have a test bean which inherits a common base class
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc(print = MockMvcPrint.LOG_DEBUG)
@TestExecutionListeners(mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS,
value = {FlywayTestExecutionListener.class}
)
@FlywayTest
@Getter(AccessLevel.PROTECTED)
public abstract class AbstractMockMvcTest {}
@Configuration
@Sql(scripts = "classpath:..", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:..", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@WithMockUser(username = "junit", authorities = {}, roles = {})
class RolesControllerTest extends AbstractMockMvcTest {
private final Instant fixedInstant = Instant.from(OffsetDateTime.of(2021, 6, 2, 15, 54, 0, 0, ZoneOffset.ofHours(2)));
@Bean
Clock fakeClock() {
return Clock.fixed(fixedInstant, ZoneId.ofOffset("UTC", ZoneOffset.ofHours(2)));
}
}
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@ConditionalOnMissingBean(Clock.class)
@Bean
public Clock systemClock() {
return Clock.systemUTC();
}
}
The very idea is that my application declares a main clock, which is used by every bean that needs to access the time (e.g. getClock().instant()
or Instant.now(getClock())
). By mocking the clock, I make testing easier.
But I found more natural using Spring beans than Mockito. Java Clock
has methods to properly mock a clock
Best practice for applications is to pass a Clock into any method that requires the current instant. A dependency injection framework is one way to achieve this:
public class MyBean {
private Clock clock; // dependency inject
...
public void process(LocalDate eventDate) {
if (eventDate.isBefore(LocalDate.now(clock)) {
...
}
}
}
This approach allows an alternate clock, such as fixed or offset to be used during testing.
But when I try to run the test, I stumble upon this
@AutoConfigureMockMvc cannot be used in combination with the @Component annotation @Configuration
Any smart idea to declare additional primary beans in a Web MVC test?
I found that the following works. This is a slightly alternative solution to @Michiel's. Initially (and before an answer was posted) I have tried their approach of an inner configuration class, but I stumbled upon another problem: the inner configuration class, either @Configuration
or @TestConfiguration
, erased the whole context and no other bean was visible. Including the PlatformTransactionManager
that could not be autowired
The following worked to me
@Configuration
@Sql(scripts = "classpath:..", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:..", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@WithMockUser(username = "junit", authorities = {}, roles = {})
class RolesControllerTest extends AbstractMockMvcTest {
private final Instant fixedInstant = Instant.from(OffsetDateTime.of(2021, 6, 2, 15, 54, 0, 0, ZoneOffset.ofHours(2)));
@TestConfiguration
@Import(Application.class)
static class ClockConfigurer {
@Bean
@Primary
Clock fakeClock() {
return Clock.fixed(fixedInstant, ZoneId.ofOffset("UTC", ZoneOffset.ofHours(2)));
}
}
}
In my case, I had to add the @Import
annotation to the test configuration class, pointing to the main configuration.
I initially used @Import
in the test class, commented it out, but every time the context was faulty. I also had to mark the clock as @Primary
in order to work with the autowired by-type resolution
This helped. Still thanks to @Michiel