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 變量名稱相同。
如果我們想使用名稱不同的工廠方法,則可以設置 @TestBean 的 methodName 屬性。
@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。