Search code examples
javaspring-bootdependency-injectionspring-boot-test

How can I efficiently use nested configuration classes to inject dependencies when I have many SpringBootTest classes


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:

  1. I tried importing the nested configuration class of one text fixture into another test fixture using the @Import annotation into the latter, but when doing so, the configuration class is ignored in the latter.
  2. I also tried moving a nested configuration class to the upper level so that it might be used for all test fixtures that do not explicitly define a different one as a nested class, but in this case the configuration class is ignored by all test fixtures.

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.


Solution

  • After some experimentation I have reached the following solution. I will add all the details summarizing what I learned in the previous question too.

    Background

    1. We have two interfaces: IClient and IServer
    2. There are two implementations of IClient: RealClient and MockClient.
    3. There are two implementations of IServer: RealServer and MockServer.

    Requirements

    1. Production code (in main/java) should use the Real implementations of both.
    2. Test Fixtures (annotated with @SpringBootTest in test/java)

      • InterfaceTests defines tests that should use a MockServer and a MockClient
      • ClientTests defines tests that should use a MockServer and RealClient to test the RealClient.
      • ServerTests defines tests that should use a MockClient and a RealServer to test the RealServer.
      • IntegrationTests defines tests that should use a RealServer and a RealClient

    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.

    Solution

    This solution makes use of the @Configuration and @TestConfiguration annotations in order to implement these requirements with no code duplication.

    1. Do NOT annotate interfaces nor their implementations with @Component
    2. Under main/java implement a configuration class as follows:
    
    @Configuration
    public class RealInjector {
        @Bean
        public IServer createServer(){
            return new RealServer();
        }
    
        @Bean
        public IClient createClient(){
            return new RealClient();
        }
    }
    
    
    1. Under test/java implement these three test configuration classes
    @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();
        }
    }
    
    
    1. Annotate the InterfaceTests test fixture as follows:
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {AllMockInjector.class})
    public class InterfaceTests { ... }
    
    1. Annotate the ClientTests test fixture as follows:
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {MockServerInjector.class})
    public class ClientTests { ... }
    
    1. Annotate the ServerTests test fixture as follows:
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {MockClientInjector.class})
    public class ServerTests { ... }
    
    1. Annotate the IntegrationTests test fixture as follows:
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {RealInjector.class})
    public class IntegrationTests { ... }
    

    Finally

    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.

    Summary

    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).