在整合測試中重寫 Spring Bean
1. 概述
我們可能想要在 Spring 整合測試中覆寫一些應用程式的 bean。通常,這可以使用專門為測試定義的 Spring Bean 來完成。然而,透過在 Spring 上下文中提供多個同名 bean,我們可能會得到BeanDefinitionOverrideException
。
本教學將展示如何在 Spring Boot 應用程式中模擬或存根整合測試 bean,同時避免BeanDefinitionOverrideException
。
2. 測試中的模擬或存根
在深入研究細節之前,我們應該對如何在測試中使用 Mock 或 Stub 有信心。這是一項強大的技術,可以確保我們的應用程式不易發生錯誤。
我們也可以將這種方法應用在 Spring。然而,只有當我們使用 Spring Boot 時,才能直接模擬整合測試 bean。
或者,我們可以使用測試來配置存根或模擬 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](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/mock/mockito/MockBean.html)
從 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
情況下覆蓋 beans 的更多選項。我們將研究四種不同的方法:Spring 配置檔案、條件屬性、 @Primary
註解和 bean 定義覆蓋。然後我們可以存根或模擬 bean 實作。
5.1.使用@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")));
}
}
我們在ProfileIntegrationTest
中啟動stub
檔。因此,不會載入prod
設定檔。因此,測試配置將載入Service
存根。
5.2.使用@ConditionalOnProperty
與設定檔類似,我們可以使用@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
的 name 屬性或方法的名稱。
同樣,我們需要一個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
屬性,因此我們可以直接覆寫 main 配置。
讓我們定義一個測試配置:
@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.使用模擬而不是存根
到目前為止,在使用測試配置時,我們已經看到了帶有存根的範例。然而,我們也可以模擬 bean。這適用於我們之前見過的任何測試配置。但是,為了進行演示,我們將遵循設定檔範例。
這次,我們使用 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
語法。
六,結論
在本教程中,我們學習如何在 Spring 整合測試期間覆寫 bean。
我們看到如何使用@MockBean
。此外,我們使用@Profile
或@ConditionalOnProperty
建立了主要配置,以便在測試期間在不同的bean之間切換。此外,我們也了解如何使用@Primary.
最後,我們看到了一個簡單的解決方案,使用spring.main.allow-bean-definition-overriding
並覆寫主配置 bean。
與往常一樣,本文中提供的程式碼可以在 GitHub 上取得。