I have tests defined as following:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemoApplication.class, TestConf.class})
public class DemoApplicationTestSBTestConfig {
@Autowired
private Bar bar;
@Test
public void test() {
org.junit.Assert.assertNotEquals(null, bar);
}
}
the TestConf class contains classes for the test context which 'override' appropriate classes from main application context (the same type/name but with different test-specific behaviour). For example:
in application context I define few beans
@Configuration
public class Conf {
@Bean
public Foo foo(){
return new Foo();
}
@Bean
public Bar bar(){
return new Bar();
}
}
then I 'override' one them in test context
@TestConfiguration
public class TestConf {
@Bean
public Bar bar(){
return Mockito.mock(Bar.class);
}
}
the expected behaviour is that in test context I will receive bar bean defined TestConf.class. But actually test context can not start (There is already [Root bean] ... defined in class path resource [com/example/demo/Conf.class]] bound).
At the same time if I explicitly use @ContextConfiguration with the same config classes the issue disappears. Eg
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {
DemoApplication.class,
TestConf.class
})
public class DemoApplicationTestContextConfig {
@Autowired
private Bar bar;
@Test
public void test() {
org.junit.Assert.assertNotEquals(null, bar);
}
}
will autowire a bean defined in TestConf.class even the same bean was already defined before.
so based on existing javadocs for classes attribute of @SpringBootTest
/**
* The <em>annotated classes</em> to use for loading an
* {@link org.springframework.context.ApplicationContext ApplicationContext}. Can also
* be specified using
* {@link ContextConfiguration#classes() @ContextConfiguration(classes=...)}. If no
* explicit classes are defined the test will look for nested
* {@link Configuration @Configuration} classes, before falling back to a
* {@link SpringBootConfiguration} search.
* @see ContextConfiguration#classes()
* @return the annotated classes used to load the application context
*/
the behaviour of ContextConfiguration#classes() and SpringBootTest#classes() should be the same.
the simplest project which demonstrate the issue is available here https://github.com/ashirman/sb2.1_test_context_priority
the behaviour was broken starting from SB 2.1 and it works as expected in 2.0.5.RELEASE
PS. I know that it is possible to use @MockBean annotation to be able to mock bean in test. In my real project I have multiple beans defined in test configuration and I am re-using the same TestConf in many tests (these are not just mocked beans but beans with specific behaviour like read data from test's resource folder rather than request via network, etc). It's not OK to mock all the beans in every test where I need it.
Thanks for the sample. The difference in behaviour is due to @SpringBootTest only being used on DemoApplicationTestSBTestConfig.
When you use @SpringBootTest the application context is bootstrapped using SpringBootTestContextBootstrapper which uses SpringApplication to create the context. This means that you are testing your application as a Spring Boot application and, since 2.1, part of being a Spring Boot application is that bean definition overriding is disabled by default.
In the absence of @SpringBootTest the context is bootstrapped using Spring Framework's default bootstrapper. This means that you are testing your application as a plain Spring Framework application and you lose the Spring Boot features and default configuration that comes from SpringApplication.
You can continue to use @SpringBootTest by configuring a spring.main.allow-bean-definition-overriding=true property:
@SpringBootTest(properties= "spring.main.allow-bean-definition-overriding=true", classes = {DemoApplication.class, TestConf.class})
This is already suggested in the failure analysis that's logged when the context fails to start:
***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'bar', defined in com.example.demo.TestConf, could not be registered. A bean with that name has already been defined in class path resource [com/example/demo/Conf.class] and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
thanks for explanation.
Most helpful comment
Thanks for the sample. The difference in behaviour is due to
@SpringBootTestonly being used onDemoApplicationTestSBTestConfig.When you use
@SpringBootTestthe application context is bootstrapped usingSpringBootTestContextBootstrapperwhich usesSpringApplicationto create the context. This means that you are testing your application as a Spring Boot application and, since 2.1, part of being a Spring Boot application is that bean definition overriding is disabled by default.In the absence of
@SpringBootTestthe context is bootstrapped using Spring Framework's default bootstrapper. This means that you are testing your application as a plain Spring Framework application and you lose the Spring Boot features and default configuration that comes fromSpringApplication.You can continue to use
@SpringBootTestby configuring aspring.main.allow-bean-definition-overriding=trueproperty:This is already suggested in the failure analysis that's logged when the context fails to start: