Search code examples
spring-bootspring-boot-test

For @SpringBootTest @Import of @TestConfiguration class does nothing, while @ContextConfiguration overrides as expected


Considering following integration test annotations:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
                properties = "spring.main.allow-bean-definition-overriding=true")
@ContextConfiguration(classes = {WorkerTestConfig.class})
//@Import(value = {WorkerTestConfig.class})
@ActiveProfiles({"dev","test"})
public class NumberServiceITest {

The role of WorkestTestConfig is to override real bean/set of beans during integration startup, whenever I use @ContextConfiguration the real bean is backing off and the one from the WorkerTestConfig is used, whenever I use @Import the real bean is still created and fails the test.

The WorkerTestConfig itself is as trivial as possible:

@TestConfiguration
public class WorkerTestConfig {

    @Primary
    @Bean
    public ScheduledExecutorService taskExecutor() {
        return DirectExecutorFactory.createSameThreadExecutor();
    }
}

can anyone explain please yet another magical behavior of @SpringBootTest annotation? If you reproduce the same behaviour please confirm so I can go to issue tracker, as I've seen people using @Import with @SpringBootTest here on SO and nothing prohibits it in spring boot docs: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html#boot-features-testing-spring-boot-applications-excluding-config

Totally puzzled on what's going on.

Version: 2.1.2.RELEASE

Update:

Also tried to remove the real bean to see if the issue is with just overriding, but @Import annotation is just dead in the water, does not work -> unable even to create a bean, @ContextConfiguration has additive/overriding behavior, import does nothing at all. Fully qualified import for the annotation is: import org.springframework.context.annotation.Import;

Also tried to change from @TestConfiguration to @Configuration just for the sake of it, nothing at all. DEAD.

Update 2:

The @Import works with standard spring test though :

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {Some.class,
                                 Some2WhichDependsOnWorkerTestConfig.class})

@Import(WorkerTestConfig.class)
@ActiveProfiles("test")
public class SomeOtherTest {

Solution

  • The order that @Import classes are processed when they are used on tests isn't defined. The @Import feature for tests was primarily added to allow additional beans to be registered easily, there was no intention of it being used to replace bean definitions.

    If you want to dig into the weeds and see exactly what's going on you can open ConfigurationClassParser and add a conditional breakpoint in doProcessConfigurationClass. Add the following condition code:

    System.err.println(configClass);
    return false;
    

    Now if you debug the application you'll get addition output as the configuration classes are processed.

    When you use classes annotation attribute without the @Import you'll see:

    ConfigurationClass: beanName 'demoImportBugApplication', com.example.demoimportbug.DemoImportBugApplication
    ConfigurationClass: beanName 'original', class path resource [com/example/demoimportbug/first/Original.class]
    ConfigurationClass: beanName 'workerConfig', class path resource [com/example/demoimportbug/first/WorkerConfig.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/scheduling/annotation/ProxyAsyncConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/scheduling/annotation/ProxyAsyncConfiguration.class]
    ConfigurationClass: beanName 'someTestSecondConfiguration', com.example.demoimportbug.second.SomeTestSecondConfiguration
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/cache/GenericCacheConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/cache/SimpleCacheConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/cache/NoOpCacheConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.class]
    

    When you use the @Import without the classes attribute you'll get:

    ConfigurationClass: beanName 'org.springframework.boot.test.context.ImportsContextCustomizer$ImportsConfiguration', org.springframework.boot.test.context.ImportsContextCustomizer$ImportsConfiguration
    ConfigurationClass: beanName 'null', class path resource [com/example/demoimportbug/first/SomeFirstUsingSecondConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [com/example/demoimportbug/second/SomeTestSecondConfiguration.class]
    ConfigurationClass: beanName 'demoImportBugApplication', com.example.demoimportbug.DemoImportBugApplication
    ConfigurationClass: beanName 'original', class path resource [com/example/demoimportbug/first/Original.class]
    ConfigurationClass: beanName 'workerConfig', class path resource [com/example/demoimportbug/first/WorkerConfig.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/scheduling/annotation/ProxyAsyncConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/scheduling/annotation/ProxyAsyncConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/cache/GenericCacheConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/cache/SimpleCacheConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/cache/NoOpCacheConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.class]
    ConfigurationClass: beanName 'null', class path resource [org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.class]
    

    The first version loads WorkerConfig before SomeTestSecondConfiguration whereas the second version loads SomeTestSecondConfiguration before WorkerConfig.

    You'll also notice that the second version has a ImportsContextCustomizer$ImportsConfiguration class which is the thing that triggers the additional imports.

    If you look at SpringBootTestContextBootstrapper you can see in the getOrFindConfigurationClasses method that ordering is defined and your additional test classes will always be listed after the primary configuration.

    tl;dr If you need defined ordering use the classes attribute. If you want to add additional beans and you aren't trying to override anything use @Import.

    You might also want to look at @MockBean which provides a more robust way to replace a bean with a mock.