知識庫 / Spring / Spring Boot RSS 訂閱

集成測試中覆蓋 Spring Bean

Spring Boot,Testing
HongKong
4
11:19 AM · Dec 06 ,2025

1. 概述

我們可能需要覆蓋應用程序中的某些 Bean 在 Spring 集成測試中的行為。通常,這可以通過為測試專門定義的 Spring Bean 來實現。然而,如果在 Spring 上下文中提供同名的多個 Bean,我們可能會遇到BeanDefinitionOverrideException

本教程將演示如何在 Spring Boot 應用程序中模擬或 stub 集成測試 Bean,同時避免出現BeanDefinitionOverrideException

2. 測試中的 Mock 或 Stub

在深入瞭解細節之前,我們應該確信如何使用 Mock 或 Stub 在測試中

這是一種強大的技術,可以確保我們的應用程序不易出現錯誤。

我們也可以使用這種方法與 Spring 結合使用。但是,如果使用 Spring Boot,直接對集成測試 Bean 進行 Mock 只有在特定條件下才可行。

或者,我們可以使用測試配置來 Stub 或 Mock 一個 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 類型的 Mock 對象。

最後,我們使用 Mockito 模擬服務返回值:

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

5. 不使用 @MockBean 覆蓋 Bean

讓我們探索在不使用 @MockBean 的情況下覆蓋 Bean 的更多選項。我們將研究四種不同的方法:Spring 配置文件、條件屬性、@Primary 註解和 Bean 定義覆蓋。然後我們可以對 Bean 實現進行樁化或模擬。

5.1. 使用 @Profile</em/>

定義配置(Profile)是 Spring 中一個廣為人知的實踐。首先,讓我們使用 @Profile 創建一個配置:

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

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

然後,我們可以使用我們的服務 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")));
    }
}

我們激活了 stub 配置文件在 ProfileIntegrationTest 中。因此,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();
    }
}

在運行時,我們需要將此條件設置為false,通常在我們的 application.properties 文件中完成:

service.stub=false

相反,在測試配置中,我們希望觸發 Service 的負載。因此,我們需要以下條件成立:

@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. 使用 Mock 代替 Stub

此前,在配置測試時,我們已經看到了一些使用 Stub 的示例。但是,我們也可以 Mock 一個 Bean。這將在我們之前見過的任何測試配置中都有效。為了演示,我們將遵循 Profile 示例。

這一次,我們使用 Mock 代替 Stub,並使用 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 注入到測試類中。與 Stub 不同,這種方法更靈活,並且將允許我們在測試用例中使用 when/then 語法。

5.6. 使用 @TestBean

註解 @TestBean 在 Spring Framework 6.2 中引入,旨在簡化測試中覆蓋 Bean 的過程。由於 Spring Boot 3.4.0 依賴 Spring Framework 6.2,因此 @TestBean 也可在 Spring Boot 3.4.0 及更高版本中使用。

接下來,讓我們通過一些示例來理解 @TestBean 如何在測試中覆蓋 Bean。

再次,我們需要實現一個 Service 樁:

class MyFakeService implements Service {
    @Override
    public String helloWorld() {
        return "Hi, there";
    }
}

然後,我們使用 @TestBean 覆蓋 Service Bean,如下所示:

@SpringBootTest(classes = { Application.class, Config.class, Endpoint.class })
@AutoConfigureMockMvc
class OverrideBeanDefinitionUsingTestBeanUnitTest {
    @Autowired
    private MockMvc mockMvc;

    @TestBean
    private Service myFakeService;

    static Service myFakeService() {
        return new MyFakeService();
    }

    @Test
    void whenUsingTestBean_thenBeanGetsOverriden() throws Exception {
        this.mockMvc.perform(get("/hello"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Hi, there")));
    }
}

在本示例中,我們將 @TestBean 註解應用於我們想要覆蓋的 Bean 變量。然後,我們創建一個無參數的 static 工廠方法,以提供 Bean 的實例。在這種情況下,myFakeService() 返回一個 MyFakeService 對象,即我們的 Service 樁。請注意,如果使用 @TestBean 註解且未設置任何屬性,則 static 工廠方法的名稱必須與 Bean 變量名稱相同。

如果我們想使用名稱不同的工廠方法,則可以設置 @TestBeanmethodName 屬性。

@TestBean(methodName = "getAnotherServiceBean")
private Service myService;
 
static Service getAnotherServiceBean() {
    return new MyFakeService();
}

當我們運行測試時,測試中 ApplicationContext 中的 Service Bean 將被返回的實例覆蓋。

如你所見,@TestBean 非常容易使用。它不需要額外的配置或第三方庫即可在測試中覆蓋 Spring Bean。

6. 結論

在本教程中,我們學習瞭如何在 Spring 集成測試中覆蓋 Bean。

我們看到了如何使用 <em @MockBean</em>>。 此外,我們使用 <em @Profile</em><em @ConditionalOnProperty</em>> 創建了主要的配置,以便在測試期間在不同的 Bean 之間切換。 此外,我們還看到了如何使用 <em @Primary</em>> 為測試 Bean 賦予更高的優先級。

此外,我們還看到了使用 <em spring.main.allow-bean-definition-overriding</em>> 的簡單解決方案,用於覆蓋主配置 Bean。

最後,如果使用 Spring 6.2 或更高版本,內置的 <em @TestBean</em>> 註解允許我們輕鬆覆蓋主配置 Bean。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.