Search code examples
javaspring-bootspring-testmockmvcjunit-jupiter

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


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?


Solution

  • 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