Overriding Spring Beans in Integration Test – 在集成测试中重写 Spring Beans

最后修改: 2023年 11月 21日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

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.

我们可能希望在Spring集成测试中覆盖应用程序的某些Bean。通常,这可以使用专门为测试定义的 Spring Beans 来实现。然而,在 Spring 上下文中提供多个同名 Bean 时,我们可能会收到 BeanDefinitionOverrideException 异常。

This tutorial will show how to mock or stub integration test beans in a Spring Boot application while avoiding the BeanDefinitionOverrideException.

本教程将展示如何在 Spring Boot 应用程序中模拟或存根集成测试 Bean,同时避免BeanDefinitionOverrideException

2. Mock or Stub in Testing

2.测试中的模拟或存根

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.

在深入了解细节之前,我们应该对如何在测试中使用Mock 或 Stub充满信心。这是一种强大的技术,可确保我们的应用程序不会出现错误。

We can also apply this approach with Spring. However, direct mocking of integration test beans is only available if we use Spring Boot.

我们也可以在 Spring 中采用这种方法。不过,只有在使用 Spring Boot 时才能直接模拟集成测试 bean。

Alternatively, we can stub or mock a bean using a test configuration.

或者,我们也可以使用测试配置来存根或模拟 bean。

3. Spring Boot Application Example

3.Spring Boot 应用实例

As an example, let’s create a simple Spring Boot application consisting of a controller, a service, and a configuration class:

例如,让我们创建一个由控制器、服务和配置类组成的简单 Spring Boot 应用程序:

@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:

/hello 端点将返回一个由服务提供的字符串,我们希望在测试过程中替换该字符串:

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:

我们还需要一个配置来加载 Service Bean:

@Configuration
public class Config {

    @Bean
    public Service helloWorld() {
        return new ServiceImpl();
    }
}

Finally, let’s add the @SpringBootApplication:

最后,让我们添加@SpringBootApplication

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

4. Overriding Using @MockBean

4.使用 @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:

MockBean从 Spring Boot 1.4.0 版本开始可用。我们不需要任何测试配置。因此,只需在我们的测试类中添加 @SpringBootTest 注解即可:

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

我们确信这与主配置没有冲突。这是因为 @MockBean 将向我们的应用程序注入一个 Service mock

Finally, we use Mockito to fake the service return:

最后,我们使用 Mockito 来伪造服务返回:

when(service.helloWorld()).thenReturn("hello mock bean");

5. Overriding Without @MockBean

5.不使用 @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.

让我们探索一下在不使用 @MockBean 的情况下覆盖 Bean 的更多选项。我们将研究四种不同的方法:Spring 配置文件、条件属性、@Primary 注解和 Bean 定义重写。然后,我们可以存根或模拟 Bean 实现。

5.1. Using @Profile

5.1.使用 @Profile

Defining profiles is a well-known practice with Spring. First, let’s create a configuration using @Profile:

定义配置文件是 Spring 众所周知的做法。首先,让我们使用 @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:

然后,我们就可以用服务 Bean 定义测试配置了:

@TestConfiguration
public class ProfileTestConfig {

    @Bean
    @Profile("stub")
    public Service helloWorld() {
        return new ProfileServiceStub();
    }
}

The ProfileServiceStub service will stub the ServiceImpl already defined:

ProfileServiceStub 服务将存根已定义的 ServiceImpl

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.

我们在 ProfileIntegrationTest 中激活了 stub 配置文件。因此,不会加载 prod 配置文件。因此,测试配置将加载 Service 存根。

5.2. Using @ConditionalOnProperty

5.2.使用 @ConditionalOnProperty

Similarly to a profile, we can use the @ConditionalOnProperty annotation to switch between different bean configurations.

与配置文件类似,我们可以使用 @ConditionalOnProperty 注解在不同的 Bean 配置之间进行切换。

Therefore, we’ll have a service.stub property in our main configuration:

因此,我们将在主配置中设置一个 service.stub 属性:

@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:

在运行时,我们需要将此条件设置为 false,通常是在 application.properties 文件中:

service.stub=false

Oppositely, in the test configuration, we want to trigger the Service load. Therefore, we need this condition to be true:

相反,在测试配置中,我们希望触发 Service 加载。因此,我们需要此条件为真:

@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:

然后,让我们添加 Service 存根:

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:

最后,让我们创建测试类。我们将 service.stub 条件设置为 true,并加载 Service 存根:

@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

5.3.使用 @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:

我们还可以使用 @Primary 注解。鉴于我们的主配置,我们可以在测试配置中定义一个主服务,以更高的优先级加载

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

值得注意的是,Bean 的名称必须不同。否则,我们仍会遇到原始异常。我们可以更改 @Bean 的 name 属性或方法的名称。

Again, we need a Service stub:

同样,我们需要一个 Service 存根:

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

5.4.使用 spring.main.allow-bean-definition-overriding 属性

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.

如果我们无法应用之前的任何选项怎么办?Spring提供了spring.main.allow-bean-definition-overriding属性,因此我们可以直接覆盖主配置

Let’s define a test configuration:

让我们定义一个测试配置:

@TestConfiguration
public class OverrideBeanDefinitionTestConfig {

    @Bean
    public Service helloWorld() {
        return new OverrideBeanDefinitionServiceStub();
    }
}

Then, we need our Service stub:

然后,我们需要 Service 存根:

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:

让我们再次创建一个测试类。如果我们要覆盖 Service Bean,就需要将属性设置为 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

5.5.使用模拟而不是存根

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.

到目前为止,在使用测试配置时,我们已经看到了使用存根的示例。不过,我们也可以 模拟 bean。这将适用于我们之前见过的任何测试配置。不过,为了进行演示,我们将以配置文件为例。

This time, instead of a stub, we return a Service using the Mockito mock method:

这一次,我们使用 Mockito mock 方法返回 Service 而不是存根

@TestConfiguration
public class ProfileTestConfig {

    @Bean
    @Profile("mock")
    public Service helloWorldMock() {
        return mock(Service.class);
    }
}

Likewise, we make a test class activating the mock profile:

同样,我们创建一个测试类,激活 mock 配置文件:

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

值得注意的是,其工作原理与 @MockBean 类似。不过,我们使用 @Autowired 注解将 Bean 注入测试类。与存根相比,这种方法更加灵活,允许我们在测试用例中直接使用 when/then 语法。

6. Conclusion

6.结论

In this tutorial, we learned how to override a bean during Spring integration testing.

在本教程中,我们学习了如何在 Spring 集成测试过程中覆盖 Bean。

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.

我们看到了如何使用 @MockBean。此外,我们还使用 @Profile@ConditionalOnProperty 创建了主配置,以便在测试过程中在不同的 Bean 之间切换。此外,我们还了解了如何使用 @Primary 为测试 Bean 赋予更高的优先级

Finally, we saw a straightforward solution using the spring.main.allow-bean-definition-overriding and override a main configuration bean.

最后,我们看到了使用 spring.main.allow-bean-definition-overriding 和覆盖主配置 Bean 的直接解决方案。

As always, the code presented in this article is available over on GitHub.

与往常一样,本文中介绍的代码可在 GitHub 上获取