使用 Spring Boot 和 @SpringBootTest 進行測試

【注】本文譯自: Testing with Spring Boot and @SpringBootTest - Reflectoring
使用@SpringBootTest 注解,Spring Boot 提供了一種方便的方法來啟動要在測試中使用的應(yīng)用程序上下文。在本教程中,我們將討論何時使用?@SpringBootTest 以及何時更好地使用其他工具進行測試。我們還將研究自定義應(yīng)用程序上下文的不同方法以及如何減少測試運行時間。
? 代碼示例
本文附有?GitHub 上的工作代碼示例。
“使用 Spring Boot 進行測試”系列
本教程是系列的一部分:
使用 Spring Boot 進行單元測試
使用 Spring Boot 和?@WebMvcTest 測試 MVC Web Controller
使用 Spring Boot 和?@DataJpaTest 測試 JPA 查詢
使用 Spring Boot 和?@SpringBootTest 進行測試
集成測試與單元測試
在開始使用 Spring Boot 進行集成測試之前,讓我們定義集成測試與單元測試的區(qū)別。
單元測試涵蓋單個“單元”,其中一個單元通常是單個類,但也可以是組合測試的一組內(nèi)聚類。
集成測試可以是以下任何一項:
涵蓋多個“單元”的測試。它測試兩個或多個內(nèi)聚類集群之間的交互。
覆蓋多個層的測試。這實際上是第一種情況的特化,例如可能涵蓋業(yè)務(wù)服務(wù)和持久層之間的交互。
涵蓋整個應(yīng)用程序路徑的測試。在這些測試中,我們向應(yīng)用程序發(fā)送請求并檢查它是否正確響應(yīng)并根據(jù)我們的預(yù)期更改了數(shù)據(jù)庫狀態(tài)。
Spring Boot 提供了?@SpringBootTest 注解,我們可以使用它來創(chuàng)建一個應(yīng)用程序上下文,其中包含我們對上述所有測試類型所需的所有對象。但是請注意,過度使用?@SpringBootTest 可能會導(dǎo)致測試套件運行時間非常長。
因此,對于涵蓋多個單元的簡單測試,我們應(yīng)該創(chuàng)建簡單的測試,與單元測試非常相似,在單元測試中,我們手動創(chuàng)建測試所需的對象圖并模擬其余部分。這樣,Spring 不會在每次測試開始時啟動整個應(yīng)用程序上下文。
測試切片
我們可以將我們的 Spring Boot 應(yīng)用程序作為一個整體來測試、一個單元一個單元地測試、也可以一層一層地測試。使用 Spring Boot 的測試切片注解,我們可以分別測試每一層。
在我們詳細研究?@SpringBootTest 注解之前,讓我們探索一下測試切片注解,以檢查?@SpringBootTest 是否真的是您想要的。
@SpringBootTest?注解加載完整的 Spring 應(yīng)用程序上下文。相比之下,測試切片注釋僅加載測試特定層所需的 bean。正因為如此,我們可以避免不必要的模擬和副作用。
@WebMvcTest
我們的 Web 控制器承擔(dān)許多職責(zé),例如偵聽 HTTP 請求、驗證輸入、調(diào)用業(yè)務(wù)邏輯、序列化輸出以及將異常轉(zhuǎn)換為正確的響應(yīng)。我們應(yīng)該編寫測試來驗證所有這些功能。
@WebMvcTest?測試切片注釋將使用剛好足夠的組件和配置來設(shè)置我們的應(yīng)用程序上下文,以測試我們的 Web 控制器層。例如,它將設(shè)置我們的@Controller、@ControllerAdvice、一個?MockMvc bean 和其他一些自動配置。
要閱讀有關(guān) @WebMvcTest 的更多信息并了解我們?nèi)绾悟炞C每個職責(zé),請閱讀我關(guān)于使用 Spring Boot 和 @WebMvcTest 測試 MVC Web 控制器的文章。
@WebFluxTest
@WebFluxTest 用于測試 WebFlux 控制器。@WebFluxTest 的工作方式類似于?@WebMvcTest 注釋,不同之處在于它不是 Web MVC 組件和配置,而是啟動 WebFlux 組件和配置。其中一個 bean 是 WebTestClient,我們可以使用它來測試我們的 WebFlux 端點。
@DataJpaTest
就像?@WebMvcTest 允許我們測試我們的 web 層一樣,@DataJpaTest 用于測試持久層。
它配置我們的實體、存儲庫并設(shè)置嵌入式數(shù)據(jù)庫?,F(xiàn)在,這一切都很好,但是,測試我們的持久層意味著什么? 我們究竟在測試什么? 如果查詢,那么什么樣的查詢?要找出所有這些問題的答案,請閱讀我關(guān)于使用 Spring Boot 和 @DataJpaTest 測試 JPA 查詢的文章。
@DataJdbcTest
Spring Data JDBC 是 Spring Data 系列的另一個成員。 如果我們正在使用這個項目并且想要測試持久層,那么我們可以使用?@DataJdbcTest 注解?。@DataJdbcTest 會自動為我們配置在我們的項目中定義的嵌入式測試數(shù)據(jù)庫和 JDBC 存儲庫。
另一個類似的項目是 Spring JDBC,它為我們提供了?JdbcTemplate 對象來執(zhí)行直接查詢。@JdbcTest?注解自動配置測試我們的 JDBC 查詢所需的?DataSource 對象。
依賴
本文中的代碼示例只需要依賴 Spring Boot 的 test starter 和 JUnit Jupiter:
dependencies {
? ? testCompile('org.springframework.boot:spring-boot-starter-test')
? ? testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
}
使用?@SpringBootTest 創(chuàng)建 ApplicationContext
@SpringBootTest?在默認(rèn)情況下開始在測試類的當(dāng)前包中搜索,然后在包結(jié)構(gòu)中向上搜索,尋找用?@SpringBootConfiguration 注解的類,然后從中讀取配置以創(chuàng)建應(yīng)用程序上下文。這個類通常是我們的主要應(yīng)用程序類,因為?@SpringBootApplication 注解包括?@SpringBootConfiguration 注解。然后,它會創(chuàng)建一個與在生產(chǎn)環(huán)境中啟動的應(yīng)用程序上下文非常相似的應(yīng)用程序上下文。
我們可以通過許多不同的方式自定義此應(yīng)用程序上下文,如下一節(jié)所述。
因為我們有一個完整的應(yīng)用程序上下文,包括 web 控制器、Spring 數(shù)據(jù)存儲庫和數(shù)據(jù)源,@SpringBootTest 對于貫穿應(yīng)用程序所有層的集成測試非常方便:
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
? @Autowired
? private MockMvc mockMvc;
? @Autowired
? private ObjectMApper objectMApper;
? @Autowired
? private UserRepository userRepository;
? @Test
? void registrationWorksThroughAllLayers() throws Exception {
? ? UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
? ? mockMvc.perform(post("/forums/{forumId}/register", 42L)
? ? ? ? ? ? .contentType("Application/json")
? ? ? ? ? ? .param("sendWelcomeMail", "true")
? ? ? ? ? ? .content(objectMApper.writeValueAsString(user)))
? ? ? ? ? ? .andExpect(status().isOk());
? ? UserEntity userEntity = userRepository.findByName("Zaphod");
? ? assertThat(userEntity.getEmail()).isEqualTo("zaphod@galaxy.net");
? }
}
@ExtendWith
本教程中的代碼示例使用 @ExtendWith?注解告訴 JUnit 5 啟用 Spring 支持。從 Spring Boot 2.1 開始,我們不再需要加載 SpringExtension,因為它作為元注釋包含在 Spring Boot 測試注釋中,例如 @DataJpaTest、@WebMvcTest 和 @SpringBootTest。
在這里,我們另外使用?@AutoConfigureMockMvc 將?MockMvc 實例添加到應(yīng)用程序上下文中。
我們使用這個?MockMvc 對象向我們的應(yīng)用程序執(zhí)行?POST 請求并驗證它是否按預(yù)期響應(yīng)。
然后,我們使用應(yīng)用程序上下文中的?UserRepository 來驗證請求是否導(dǎo)致數(shù)據(jù)庫狀態(tài)發(fā)生預(yù)期的變化。
自定義應(yīng)用程序上下文
我們可以有很多種方法來自定義?@SpringBootTest 創(chuàng)建的應(yīng)用程序上下文。讓我們看看我們有哪些選擇。
自定義應(yīng)用上下文時的注意事項
應(yīng)用程序上下文的每個自定義都是使其與在生產(chǎn)設(shè)置中啟動的“真實”應(yīng)用程序上下文不同的另一件事。因此,為了使我們的測試盡可能接近生產(chǎn),我們應(yīng)該只定制讓測試運行真正需要的東西!
添加自動配置
在上面,我們已經(jīng)看到了自動配置的作用:
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
? ...
}
還有很多其他可用的自動配置,每個都可以將其他 bean 添加到應(yīng)用程序上下文中。以下是文檔中其他一些有用的內(nèi)容:
@AutoConfigureWebTestClient:將?WebTestClient 添加到測試應(yīng)用程序上下文。它允許我們測試服務(wù)器端點。
@AutoConfigureTestDatabase:這允許我們針對真實數(shù)據(jù)庫而不是嵌入式數(shù)據(jù)庫運行測試。
@RestClientTest:當(dāng)我們想要測試我們的?RestTemplate 時它會派上用場。 它自動配置所需的組件以及一個?MockRestServiceServer 對象,該對象幫助我們模擬來自?RestTemplate 調(diào)用的請求的響應(yīng)。
@JsonTest:自動配置 JSON 映射器和類,例如?JacksonTester 或 GsonTester。使用這些我們可以驗證我們的 JSON 序列化/反序列化是否正常工作。
設(shè)置自定義配置屬性
通常,在測試中需要將一些配置屬性設(shè)置為與生產(chǎn)設(shè)置中的值不同的值:
@SpringBootTest(properties = "foo=bar")
class SpringBootPropertiesTest {
? @Value("${foo}")
? String foo;
? @Test
? void test(){
? ? assertThat(foo).isEqualTo("bar");
? }
}
如果屬性?foo 存在于默認(rèn)設(shè)置中,它將被此測試的值?bar 覆蓋。
使用 @ActiveProfiles 外部化屬性
如果我們的許多測試需要相同的屬性集,我們可以創(chuàng)建一個配置文件?Application-<profile>.propertie 或?Application-<profile>.yml 并通過激活某個配置文件從該文件加載屬性:
# Application-test.yml foo: bar
@SpringBootTest
@ActiveProfiles("test")
class SpringBootProfileTest {
? @Value("${foo}")
? String foo;
? @Test
? void test(){
? ? assertThat(foo).isEqualTo("bar");
? }
}
使用?@TestPropertySource 設(shè)置自定義屬性
另一種定制整個屬性集的方法是使用?@TestPropertySource 注釋:
# src/test/resources/foo.properties
foo=bar
@SpringBootTest
@TestPropertySource(locations = "/foo.properties")
class SpringBootPropertySourceTest {
? @Value("${foo}")
? String foo;
? @Test
? void test(){
? ? assertThat(foo).isEqualTo("bar");
? }
}
foo.properties?文件中的所有屬性都加載到應(yīng)用程序上下文中。@TestPropertySource 還可以配置更多。
使用 @MockBean 注入模擬
如果我們只想測試應(yīng)用程序的某個部分而不是從傳入請求到數(shù)據(jù)庫的整個路徑,我們可以使用?@MockBean 替換應(yīng)用程序上下文中的某些 bean:
@SpringBootTest
class MockBeanTest {
? @MockBean
? private UserRepository userRepository;
? @Autowired
? private RegisterUseCase registerUseCase;
? @Test
? void testRegister(){
? ? // given
? ? User user = new User("Zaphod", "zaphod@galaxy.net");
? ? boolean sendWelcomeMail = true;
? ? given(userRepository.save(any(UserEntity.class))).willReturn(userEntity(1L));
? ? // when
? ? Long userId = registerUseCase.registerUser(user, sendWelcomeMail);
? ? // then
? ? assertThat(userId).isEqualTo(1L);
? }
?
}
在這種情況下,我們用模擬替換了?UserRepository bean。 使用 Mockito 的?given 方法,我們指定了此模擬的預(yù)期行為,以測試使用此存儲庫的類。
您可以在我關(guān)于模擬的文章中閱讀有關(guān)?@MockBean 注解的更多信息。
使用?@Import 添加 Bean
如果某些 bean 未包含在默認(rèn)應(yīng)用程序上下文中,但我們在測試中需要它們,我們可以使用?@Import 注解導(dǎo)入它們:
package other.namespace;
@Component
public class Foo {
}
@SpringBootTest
@Import(other.namespace.Foo.class)
class SpringBootImportTest {
? @Autowired
? Foo foo;
? @Test
? void test() {
? ? assertThat(foo).isNotNull();
? }
}
默認(rèn)情況下,Spring Boot 應(yīng)用程序包含它在其包和子包中找到的所有組件,因此通常只有在我們想要包含其他包中的 bean 時才需要這樣做。
使用?@TestConfiguration 覆蓋 Bean
使用 @TestConfiguration,我們不僅可以包含測試所需的其他 bean,還可以覆蓋應(yīng)用程序中已經(jīng)定義的 bean。在我們關(guān)于使用 @TestConfiguration 進行測試的文章中閱讀更多相關(guān)信息。
創(chuàng)建自定義 @SpringBootApplication
我們甚至可以創(chuàng)建一個完整的自定義 Spring Boot 應(yīng)用程序來啟動測試。如果這個應(yīng)用程序類與真正的應(yīng)用程序類在同一個包中,但是在測試源而不是生產(chǎn)源中,@SpringBootTest 會在實際應(yīng)用程序類之前找到它,并從這個應(yīng)用程序加載應(yīng)用程序上下文。
或者,我們可以告訴 Spring Boot 使用哪個應(yīng)用程序類來創(chuàng)建應(yīng)用程序上下文:
@SpringBootTest(classes = CustomApplication.class)
class CustomApplicationTest {
}
但是,在執(zhí)行此操作時,我們正在測試可能與生產(chǎn)環(huán)境完全不同的應(yīng)用程序上下文,因此僅當(dāng)無法在測試環(huán)境中啟動生產(chǎn)應(yīng)用程序時,這才應(yīng)該是最后的手段。但是,通常有更好的方法,例如使真實的應(yīng)用程序上下文可配置以排除不會在測試環(huán)境中啟動的 bean。讓我們看一個例子。
假設(shè)我們在應(yīng)用程序類上使用?@EnableScheduling?注解。每次啟動應(yīng)用程序上下文時(即使在測試中),所有?@Scheduled 作業(yè)都將啟動,并且可能與我們的測試沖突。 我們通常不希望作業(yè)在測試中運行,因此我們可以創(chuàng)建第二個沒有?@EnabledScheduling 注釋的應(yīng)用程序類,并在測試中使用它。 但是,更好的解決方案是創(chuàng)建一個可以使用屬性切換的配置類:
@Configuration
@EnableScheduling
@ConditionalOnProperty(
? ? ? ? name = "io.reflectoring.scheduling.enabled",
? ? ? ? havingValue = "true",
? ? ? ? matchIfMissing = true)
public class SchedulingConfiguration {
}
我們已將?@EnableScheduling 注解從我們的應(yīng)用程序類移到這個特殊的配置類。將屬性?io.reflectoring.scheduling.enabled 設(shè)置為?false 將導(dǎo)致此類不會作為應(yīng)用程序上下文的一部分加載:
@SpringBootTest(properties = "io.reflectoring.scheduling.enabled=false")
class SchedulingTest {
? @Autowired(required = false)
? private SchedulingConfiguration schedulingConfiguration;
? @Test
? void test() {
? ? assertThat(schedulingConfiguration).isNull();
? }
}
我們現(xiàn)在已經(jīng)成功地停用了測試中的預(yù)定作業(yè)。屬性?io.reflectoring.scheduling.enabled 可以通過上述任何方式指定。
為什么我的集成測試這么慢?
包含大量?@SpringBootTest 注釋測試的代碼庫可能需要相當(dāng)長的時間才能運行。Spring 的測試支持?足夠智能?,只創(chuàng)建一次應(yīng)用上下文并在后續(xù)測試中重復(fù)使用,但是如果不同的測試需要不同的應(yīng)用上下文,它仍然會為每個測試創(chuàng)建一個單獨的上下文,這需要一些時間來完成每個測試。
上面描述的所有自定義選項都會導(dǎo)致 Spring 創(chuàng)建一個新的應(yīng)用程序上下文。因此,我們可能希望創(chuàng)建一個配置并將其用于所有測試,以便可以重用應(yīng)用程序上下文。
如果您對測試花費在設(shè)置和 Spring 應(yīng)用程序上下文上的時間感興趣,您可能需要查看?JUnit Insights,它可以包含在 Gradle 或 Maven 構(gòu)建中,以生成關(guān)于 JUnit 5 如何花費時間的很好的報告。
結(jié)論
@SpringBootTest?是一種為測試設(shè)置應(yīng)用程序上下文的非常方便的方法,它非常接近我們將在生產(chǎn)中使用的上下文。有很多選項可以自定義此應(yīng)用程序上下文,但應(yīng)謹(jǐn)慎使用它們,因為我們希望我們的測試盡可能接近生產(chǎn)運行。
如果我們想在整個應(yīng)用程序中進行測試,@SpringBootTest 會帶來最大的價值。為了僅測試應(yīng)用程序的某些切片或?qū)樱覀冞€有其他選項可用。
本文中使用的示例代碼可在?githu 上找到。