Injecting a bean into the context of a WebMvc test: @AutoConfigureMockMvc cannot be used in combination with the @Component annotation @Configuration

Viewed 512

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?

2 Answers

Set up a static inner @TestConfiguration class in which the bean is defined:

// Remove the @Configuration annotation here
@Sql(scripts = "classpath:..", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:..", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@WithMockUser(username = "junit", authorities = {}, roles = {})
@ContextConfiguration(classes = RolesControllerTest.TestConfiguration.class) // Add a ContextConfiguration if the test configuration isn't automatically used.
class RolesControllerTest extends AbstractMockMvcTest {

    private final Instant fixedInstant = Instant.from(OffsetDateTime.of(2021, 6, 2, 15, 54, 0, 0, ZoneOffset.ofHours(2)));

    @TestConfiguration
    public static class TestConfig {

        @Bean
        Clock fakeClock() {
            return Clock.fixed(fixedInstant, ZoneId.ofOffset("UTC", ZoneOffset.ofHours(2)));
        }
    }
}

An alternative is to declare a @MockBean private Clock clock and use mockito to return the fixed instant.

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

Related