This question is a sequel to Can I use code to control the dependency resolution decisions made by ApplicationContext in Spring Boot?
The accepted answer is to define a nested class within each @SpringBootTest
test fixture class, to annotate it with @TestConfiguration
and to define within it a factory method for each bean that needs to be resolved. The influence of the nested classes is scoped to the test fixture affecting all of the tests in the fixture but not affecting tests defined in other fixtures.
This provides fine grained control over the dependencies injected into the components when running the tests in each test fixture.
The problem with this approach is that it requires adding a nested resolver class within each test fixture class. This is not scalable. Consider a project with 10 test fixtures. 9 of these use the same injected dependencies and only the 10th requires a different implementation for only one particular interface.
In this case I would need to copy the test configuration class into 9 test fixture classes and use a second configuration class for only the 10th test.
I need a more scalable way to do this. For instance, in the case above, I would like to be able to define two configuration classes, one for each of the two configurations used by the test fixtures. Then I would like to be able specify for each test fixture which of the two configuration classes should be used. I have tried:
@Import
annotation into
the latter, but when doing so, the configuration class is ignored in
the latter. So in summary I am looking for an efficient way that would allow me to write each configuration class only once and then to selectively apply one to each SpringBootTest class without needing to copy it.
After some experimentation I have reached the following solution. I will add all the details summarizing what I learned in the previous question too.
Test Fixtures (annotated with @SpringBootTest in test/java)
From the above it is clear that there are four combinations of mock/real client/server and each combination is needed in some area of the code.
This solution makes use of the @Configuration and @TestConfiguration annotations in order to implement these requirements with no code duplication.
@Configuration
public class RealInjector {
@Bean
public IServer createServer(){
return new RealServer();
}
@Bean
public IClient createClient(){
return new RealClient();
}
}
@TestConfiguration
public class AllMockInjector {
@Bean
public IServer createServer(){
return new MockServer();
}
@Bean
public IClient createClient(){
return new MockClient();
}
}
@TestConfiguration
public class MockServerInjector{
@Bean
public IServer createServer(){
return new MockServer();
}
@Bean
public IClient createClient(){
return new RealClient();
}
}
@TestConfiguration
public class MockClientInjector{
@Bean
public IServer createServer(){
return new RealServer();
}
@Bean
public IClient createClient(){
return new MockClient();
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {AllMockInjector.class})
public class InterfaceTests { ... }
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MockServerInjector.class})
public class ClientTests { ... }
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MockClientInjector.class})
public class ServerTests { ... }
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {RealInjector.class})
public class IntegrationTests { ... }
In order for the test configuration classes to override the RealInjector configuration class from main/java we need to set the property:
spring.main.allow-bean-definition-overriding=true
One way to do this is to annotate each of the above test fixtures as follows:
@SpringBootTest(properties = ["spring.main.allow-bean-definition-overriding=true"])
class TestFixture { ... }
but this is quite verbose especially if you have many test fixtures. Instead you can add the following in the application.properties file under test/resources:
spring.main.allow-bean-definition-overriding=true
You may also need to add it in application.properties under main/resources too.
This solution gives you fine grained control over the implementations that are injected into your code for production and for tests. The solution requires no code duplication or external configuration files (apart from one property in test/resources/application.properties).