1. 概述

在进行Spring集成测试时,我们可能希望覆盖应用中的一些bean。通常,这可以通过专门为测试定义的Spring Bean来实现。然而,如果在Spring上下文中为同一个名称提供了多个bean,可能会引发BeanDefinitionOverrideException

本教程将展示如何在Spring Boot应用中模拟或替代理论测试bean,同时避免BeanDefinitionOverrideException

2. 测试中的模拟或替代理论

在深入细节之前,我们应该熟悉如何在测试中使用模拟替代理论**。这是一种强大的技术,确保我们的应用不易出现错误。

我们也可以将此方法应用于Spring。然而,在直接模拟集成测试bean方面,只有在使用Spring Boot时才可行。

另一种选择是使用测试配置来替代理论或模拟一个bean。

3. 示例Spring Boot应用

作为一个示例,让我们创建一个简单的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();
    }
}

/hello端点会返回由我们希望在测试中替换的服务提供的字符串:

public interface Service {
    String helloWorld();
}

public class ServiceImpl implements Service {

    public String helloWorld() {
        return "hello world";
    }
}

值得注意的是,我们将使用接口。因此,在需要时,我们可以替代理论实现以获取不同的值。

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

@Configuration
public class Config {

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

最后,添加@SpringBootApplication注解:

@SpringBootApplication
public class Application {

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

4. 使用@MockBean覆盖

@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")));
    }
}

我们有把握不会与主配置冲突。这是因为@MockBean将在我们的应用中注入一个Service模拟。

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

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

5. 不使用@MockBean的替代方案

接下来,我们将探讨不使用@MockBean的情况下替换bean的更多选项。我们将探讨四种不同的方法:Spring Profile、条件属性、@Primary注解以及bean定义覆盖。然后,我们可以替代理论或模拟bean的实现。

5.1. 使用@Profile

在Spring中,使用Profile是常见的做法。首先,我们创建一个使用@Profile`的配置:

@Configuration
@Profile("prod")
public class ProfileConfig {

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

然后,我们可以定义包含Service bean的测试配置:

@TestConfiguration
public class ProfileTestConfig {

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

ProfileServiceStub服务将替代理论已经定义的ServiceImpl

public class ProfileServiceStub implements Service {

    public String helloWorld() {
        return "hello profile stub";
    }
}

我们可以创建一个包含主配置和测试配置的测试类:

@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")));
    }
}

我们在ProfileIntegrationTest中激活stub配置。因此,不会加载prod配置,这样测试配置就会加载Service替代理论。

5.2. 使用@ConditionalOnProperty

类似于Profile,我们可以使用@ConditionalOnProperty注解在不同的bean配置之间切换。

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

@Configuration
public class ConditionalConfig {

    @Bean
    @ConditionalOnProperty(name = "service.stub", havingValue = "false")
    public Service helloWorld() {
        return new ServiceImpl();
    }
}

运行时,我们需要在application.properties文件中设置这个条件为false:

service.stub=false

相反,在测试配置中,我们需要触发Service的加载。所以,这个条件需要为true:

@TestConfiguration
public class ConditionalTestConfig {

    @Bean
    @ConditionalOnProperty(name="service.stub", havingValue="true")
    public Service helloWorld() {
        return new ConditionalStub();
    }
}

然后,我们也需要添加Service替代理论:

public class ConditionalStub implements Service {

    public String helloWorld() {
        return "hello conditional 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. 使用@Primary

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

@TestConfiguration
public class PrimaryTestConfig {

    @Primary
    @Bean("service.stub")
    public Service helloWorld() {
        return new PrimaryServiceStub();
    }
}

重要的是,bean的名字需要不同,否则仍会遇到原始异常。我们可以更改@Bean的名称属性或方法名。

同样,我们需要一个Service替代理论:

public class PrimaryServiceStub implements Service {

    public String helloWorld() {
        return "hello primary stub";
    }
}

最后,创建我们的测试类,定义所有相关组件:

@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. 使用spring.main.allow-bean-definition-overriding属性

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

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

@TestConfiguration
public class OverrideBeanDefinitionTestConfig {

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

然后,我们需要Service替代理论:

public class OverrideBeanDefinitionServiceStub implements Service {

    public String helloWorld() {
        return "hello no profile stub";
    }
}

再次,创建我们的测试类。如果我们要覆盖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. 使用模拟而不是替代理论

到目前为止,当我们使用测试配置时,看到的都是替代理论的例子。但是,我们也可以使用Mockito模拟一个bean。这适用于我们之前见过的所有测试配置。为了演示,我们将遵循Profile的例子。

这次,我们不再使用替代理论,而是使用Mockito的mock方法返回一个Service

@TestConfiguration
public class ProfileTestConfig {

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

同样,我们创建一个激活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")));
    }
}

值得注意的是,这与@MockBean的工作方式相似。然而,我们使用@Autowired注解将bean注入到测试类中。与替代理论相比,这种方法更灵活,允许我们在测试用例中直接使用when/then语法。

6. 总结

在这篇教程中,我们学习了如何在Spring集成测试中覆盖bean。

我们了解了如何使用@MockBean,并创建了使用@Profile@ConditionalOnProperty在测试期间切换不同bean的主配置。此外,我们还看到了如何通过@Primary赋予测试bean更高的优先级。

最后,我们看到了一个直接使用spring.main.allow-bean-definition-overriding属性覆盖主配置的简单解决方案。

如往常一样,本文中的代码可在GitHub上找到。