1. Overview
We might want to override some of our application’s beans in Spring integration testing. Typically, this can be done using Spring Beans specifically defined for testing. However, by providing more than one bean with the same name in a Spring context, we might get a BeanDefinitionOverrideException.
This tutorial will show how to mock or stub integration test beans in a Spring Boot application while avoiding the BeanDefinitionOverrideException.
2. Mock or Stub in Testing
Before digging into the details, we should be confident in how to use a Mock or Stub in testing. This is a powerful technique to make sure our application is not prone to bugs.
We can also apply this approach with Spring. However, direct mocking of integration test beans is only available if we use Spring Boot.
Alternatively, we can stub or mock a bean using a test configuration.
3. Spring Boot Application Example
As an example, let’s create a simple Spring Boot application consisting of a controller, a service, and a configuration class:
@RestController
public class Endpoint {
private final Service service;
public Endpoint(Service service) {
this.service = service;
}
@GetMapping("/hello")
public String helloWorldEndpoint() {
return service.helloWorld();
}
}
The /hello endpoint will return a string provided by a service that we want to replace during testing:
public interface Service {
String helloWorld();
}
public class ServiceImpl implements Service {
public String helloWorld() {
return "hello world";
}
}
Notably, we’ll use an interface. Therefore, when required, we’ll stub the implementation to get a different value.
We also need a configuration to load the Service bean:
@Configuration
public class Config {
@Bean
public Service helloWorld() {
return new ServiceImpl();
}
}
Finally, let’s add the @SpringBootApplication:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4. Overriding Using @MockBean
MockBean has been available since version 1.4.0 of Spring Boot. We don’t need any test configuration. Therefore, it’s sufficient to add the @SpringBootTest annotation to our test class:
@SpringBootTest(classes = { Application.class, Endpoint.class })
@AutoConfigureMockMvc
class MockBeanIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private Service service;
@Test
void givenServiceMockBean_whenGetHelloEndpoint_thenMockOk() throws Exception {
when(service.helloWorld()).thenReturn("hello mock bean");
this.mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello mock bean")));
}
}
We are confident that there is no conflict with the main configuration. This is because @MockBean will inject a Service mock into our application.
Finally, we use Mockito to fake the service return:
when(service.helloWorld()).thenReturn("hello mock bean");
5. Overriding Without @MockBean
Let’s explore more options for overriding beans without @MockBean. We’ll look at four different approaches: Spring profiles, conditional properties, the @Primary annotation, and bean definition overriding. We can then stub or mock the bean implementation.
5.1. Using @Profile
Defining profiles is a well-known practice with Spring. First, let’s create a configuration using @Profile:
@Configuration
@Profile("prod")
public class ProfileConfig {
@Bean
public Service helloWorld() {
return new ServiceImpl();
}
}
Then, we can define a test configuration with our service bean:
@TestConfiguration
public class ProfileTestConfig {
@Bean
@Profile("stub")
public Service helloWorld() {
return new ProfileServiceStub();
}
}
The ProfileServiceStub service will stub the ServiceImpl already defined:
public class ProfileServiceStub implements Service {
public String helloWorld() {
return "hello profile stub";
}
}
We can create a test class including the main and test configuration:
@SpringBootTest(classes = { Application.class, ProfileConfig.class, Endpoint.class, ProfileTestConfig.class })
@AutoConfigureMockMvc
@ActiveProfiles("stub")
class ProfileIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void givenConfigurationWithProfile_whenTestProfileIsActive_thenStubOk() throws Exception {
this.mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello profile stub")));
}
}
We activate the stub profile in the ProfileIntegrationTest. Therefore, the prod profile is not loaded. Thus, the test configuration will load the Service stub.
5.2. Using @ConditionalOnProperty
Similarly to a profile, we can use the @ConditionalOnProperty annotation to switch between different bean configurations.
Therefore, we’ll have a service.stub property in our main configuration:
@Configuration
public class ConditionalConfig {
@Bean
@ConditionalOnProperty(name = "service.stub", havingValue = "false")
public Service helloWorld() {
return new ServiceImpl();
}
}
At runtime, we need to set this condition to false, typically in our application.properties file:
service.stub=false
Oppositely, in the test configuration, we want to trigger the Service load. Therefore, we need this condition to be true:
@TestConfiguration
public class ConditionalTestConfig {
@Bean
@ConditionalOnProperty(name="service.stub", havingValue="true")
public Service helloWorld() {
return new ConditionalStub();
}
}
Then, let’s also add our Service stub:
public class ConditionalStub implements Service {
public String helloWorld() {
return "hello conditional stub";
}
}
Finally, let’s create our test class. We’ll set the service.stub conditional to true and load the Service stub:
@SpringBootTest(classes = { Application.class, ConditionalConfig.class, Endpoint.class, ConditionalTestConfig.class }
, properties = "service.stub=true")
@AutoConfigureMockMvc
class ConditionIntegrationTest {
@AutowiredService
private MockMvc mockMvc;
@Test
void givenConditionalConfig_whenServiceStubIsTrue_thenStubOk() throws Exception {
this.mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello conditional stub")));
}
}
5.3. Using @Primary
We can also use the @Primary annotation. Given our main configuration, we can define a primary service in a test configuration to be loaded with higher priority:
@TestConfiguration
public class PrimaryTestConfig {
@Primary
@Bean("service.stub")
public Service helloWorld() {
return new PrimaryServiceStub();
}
}
Notably, the bean’s name needs to be different. Otherwise, we’ll still bump into the original exception. We can change the name property of @Bean or the method’s name.
Again, we need a Service stub:
public class PrimaryServiceStub implements Service {
public String helloWorld() {
return "hello primary stub";
}
}
Finally, let’s create our test class by defining all relevant components:
@SpringBootTest(classes = { Application.class, NoProfileConfig.class, Endpoint.class, PrimaryTestConfig.class })
@AutoConfigureMockMvc
class PrimaryIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void givenTestConfiguration_whenPrimaryBeanIsDefined_thenStubOk() throws Exception {
this.mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello primary stub")));
}
}
5.4. Using spring.main.allow-bean-definition-overriding Property
What if we can’t apply any of the previous options? Spring provides the spring.main.allow-bean-definition-overriding property so we can directly override the main configuration.
Let’s define a test configuration:
@TestConfiguration
public class OverrideBeanDefinitionTestConfig {
@Bean
public Service helloWorld() {
return new OverrideBeanDefinitionServiceStub();
}
}
Then, we need our Service stub:
public class OverrideBeanDefinitionServiceStub implements Service {
public String helloWorld() {
return "hello no profile stub";
}
}
Again, let’s create a test class. If we want to override the Service bean, we need to set our property to true:
@SpringBootTest(classes = { Application.class, Config.class, Endpoint.class, OverribeBeanDefinitionTestConfig.class },
properties = "spring.main.allow-bean-definition-overriding=true")
@AutoConfigureMockMvc
class OverrideBeanDefinitionIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void givenNoProfile_whenAllowBeanDefinitionOverriding_thenStubOk() throws Exception {
this.mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello no profile stub")));
}
}
5.5. Using a Mock Instead of a Stub
So far, while using test configuration, we have seen examples with stubs. However, we can also mock a bean. This will work for any test configuration we have seen previously. However, to demonstrate, we’ll follow the profile example.
This time, instead of a stub, we return a Service using the Mockito mock method:
@TestConfiguration
public class ProfileTestConfig {
@Bean
@Profile("mock")
public Service helloWorldMock() {
return mock(Service.class);
}
}
Likewise, we make a test class activating the mock profile:
@SpringBootTest(classes = { Application.class, ProfileConfig.class, Endpoint.class, ProfileTestConfig.class })
@AutoConfigureMockMvc
@ActiveProfiles("mock")
class ProfileIntegrationMockTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private Service service;
@Test
void givenConfigurationWithProfile_whenTestProfileIsActive_thenMockOk() throws Exception {
when(service.helloWorld()).thenReturn("hello profile mock");
this.mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello profile mock")));
}
}
Notably, this works similarly to the @MockBean. However, we use the @Autowired annotation to inject a bean into the test class. Compared to a stub, this approach is more flexible and will allow us to directly use the when/then syntax inside the test cases.
6. Conclusion
In this tutorial, we learned how to override a bean during Spring integration testing.
We saw how to use @MockBean. Furthermore, we created the main configuration using @Profile or @ConditionalOnProperty to switch between different beans during tests. Also, we have seen how to give a higher priority to a test bean using @Primary.
Finally, we saw a straightforward solution using the spring.main.allow-bean-definition-overriding and override a main configuration bean.
As always, the code presented in this article is available over on GitHub.