I am testing a Spring Boot application. I have several test classes, each of which needs a different set of mocked or otherwise customized beans.
Here is a sketch of the setup:
src/main/java:
package com.example.myapp;
@SpringBootApplication
@ComponentScan(
basePackageClasses = {
MyApplication.class,
ImportantConfigurationFromSomeLibrary.class,
ImportantConfigurationFromAnotherLibrary.class})
@EnableFeignClients
@EnableHystrix
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
package com.example.myapp.feature1;
@Component
public class Component1 {
@Autowired
ServiceClient serviceClient;
@Autowired
SpringDataJpaRepository dbRepository;
@Autowired
ThingFromSomeLibrary importantThingIDontWantToExplicitlyConstructInTests;
// methods I want to test...
}
src/test/java:
package com.example.myapp;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@WebAppConfiguration
@ActiveProfiles("test")
public class Component1TestWithFakeCommunication {
@Autowired
Component1 component1; // <-- the thing we're testing. wants the above mock implementations of beans wired into it.
@Autowired
ServiceClient mockedServiceClient;
@Configuration
static class ContextConfiguration {
@Bean
@Primary
public ServiceClient mockedServiceClient() {
return mock(ServiceClient.class);
}
}
@Before
public void setup() {
reset(mockedServiceClient);
}
@Test
public void shouldBehaveACertainWay() {
// customize mock, call component methods, assert results...
}
}
package com.example.myapp;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@WebAppConfiguration
@ActiveProfiles("test")
public class Component1TestWithRealCommunication {
@Autowired
Component1 component1; // <-- the thing we're testing. wants the real implementations in this test.
@Autowired
ServiceClient mockedServiceClient;
@Before
public void setup() {
reset(mockedServiceClient);
}
@Test
public void shouldBehaveACertainWay() {
// call component methods, assert results...
}
}
The problem with the above setup is that the component scan configured in MyApplication picks up Component1TestWithFakeCommunication.ContextConfiguration, so I get a mock ServiceClient even in Component1TestWithRealCommunication where I want the real ServiceClient implementation.
Although I could use @Autowired constructors and build up the components myself in both tests, there is a sufficient amount of stuff with complicated setup that I would rather have Spring TestContext set up for me (for example, Spring Data JPA repositories, components from libraries outside the app that pull beans from the Spring context, etc.). Nesting a Spring configuration inside the test that can locally override certain bean definitions within the Spring context feels like it should be a clean way to do this; the only downfall is that these nested configurations end up affecting all Spring TestContext tests that base their configuration on MyApplication (which component scans the app package).
How do I modify my setup so I still get a "mostly real" Spring context for my tests with just a few locally overridden beans in each test class?
You may use additional explicit profiles to avoid such test configurations to be picked up (as suggested in another answer). I also did it and even created some library support for that.
However, Spring-Boot is clever and it has a built-in "type filter" to resolve this issue automatically. For this to work, you need to remove your
@ComponentScan
annotation, which would find your test configurations, and let the@SpringBootApplication
do the work. In your example, just remove this:and replace it with:
You may also need to annotate your test as
@SpringBootTest
. This should avoid auto-scanning any inner-class configurations (and components) except for those residing in the current test.If you have a
@SpringBootTest
you can just annotate the service you want to mock with@MockBean
. As simple as that.The following should help you to achieve your goal by introducing a new
fake-communication
profile that is applicable only to the current test class.I would do a couple of things:
@ComponentScan
ing them.Component1TestWithFakeCommunication
, change@SpringApplicationConfiguration(classes = MyApplication.class)
to@SpringApplicationConfiguration(classes = {MyApplication.class, Component1TestWithFakeCommunication.ContextConfiguration.class})
That should give Spring sufficient information to mock up the test beans, but it should prevent the runtime
ApplicationContext
from noticing your test beans as well.