【注】本文譯自: Testing with Spring Boot and @SpringBootTest - Reflectoring
使用@SpringBootTest
注解,Spring Boot 提供了一種方便的方法來啟動要在測試中使用的應用程序上下文。在本教程中,我們將討論何時使用 @SpringBootTest
以及何時更好地使用其他工具進行測試。我們還將研究自定義應用程序上下文的不同方法以及如何減少測試運行時間。
代碼示例
本文附有 GitHub 上的工作代碼示例。
“使用 Spring Boot 進行測試”系列
本教程是系列的一部分:
- 使用 Spring Boot 進行單元測試
- 使用 Spring Boot 和 @WebMvcTest 測試 MVC Web Controller
- 使用 Spring Boot 和 @DataJpaTest 測試 JPA 查詢
- 使用 Spring Boot 和 @SpringBootTest 進行測試
集成測試與單元測試
在開始使用 Spring Boot 進行集成測試之前,讓我們定義集成測試與單元測試的區別。
單元測試涵蓋單個“單元”,其中一個單元通常是單個類,但也可以是組合測試的一組內聚類。
集成測試可以是以下任何一項:
- 涵蓋多個“單元”的測試。它測試兩個或多個內聚類集群之間的交互。
- 覆蓋多個層的測試。這實際上是第一種情況的特化,例如可能涵蓋業務服務和持久層之間的交互。
- 涵蓋整個應用程序路徑的測試。在這些測試中,我們向應用程序發送請求並檢查它是否正確響應並根據我們的預期更改了數據庫狀態。
Spring Boot 提供了 @SpringBootTest
注解,我們可以使用它來創建一個應用程序上下文,其中包含我們對上述所有測試類型所需的所有對象。但是請注意,過度使用 @SpringBootTest
可能會導致測試套件運行時間非常長。
因此,對於涵蓋多個單元的簡單測試,我們應該創建簡單的測試,與單元測試非常相似,在單元測試中,我們手動創建測試所需的對象圖並模擬其余部分。這樣,Spring 不會在每次測試開始時啟動整個應用程序上下文。
測試切片
我們可以將我們的 Spring Boot 應用程序作為一個整體來測試、一個單元一個單元地測試、也可以一層一層地測試。使用 Spring Boot 的測試切片注解,我們可以分別測試每一層。
在我們詳細研究 @SpringBootTest
注解之前,讓我們探索一下測試切片注解,以檢查 @SpringBootTest
是否真的是您想要的。
@SpringBootTest
注解加載完整的 Spring 應用程序上下文。相比之下,測試切片注釋僅加載測試特定層所需的 bean。正因為如此,我們可以避免不必要的模擬和副作用。
@WebMvcTest
我們的 Web 控制器承擔許多職責,例如偵聽 HTTP 請求、驗證輸入、調用業務邏輯、序列化輸出以及將異常轉換為正確的響應。我們應該編寫測試來驗證所有這些功能。
@WebMvcTest
測試切片注釋將使用剛好足夠的組件和配置來設置我們的應用程序上下文,以測試我們的 Web 控制器層。例如,它將設置我們的@Controller
、@ControllerAdvice
、一個 MockMvc
bean 和其他一些自動配置。
要閱讀有關 @WebMvcTest 的更多信息並了解我們如何驗證每個職責,請閱讀我關於使用 Spring Boot 和 @WebMvcTest 測試 MVC Web 控制器的文章。
@WebFluxTest
@WebFluxTest
用於測試 WebFlux 控制器。 @WebFluxTest
的工作方式類似於 @WebMvcTest
注釋,不同之處在於它不是 Web MVC 組件和配置,而是啟動 WebFlux 組件和配置。其中一個 bean 是 WebTestClient,我們可以使用它來測試我們的 WebFlux 端點。
@DataJpaTest
就像 @WebMvcTest
允許我們測試我們的 web 層一樣,@DataJpaTest
用於測試持久層。
它配置我們的實體、存儲庫並設置嵌入式數據庫。現在,這一切都很好,但是,測試我們的持久層意味着什么? 我們究竟在測試什么? 如果查詢,那么什么樣的查詢?要找出所有這些問題的答案,請閱讀我關於使用 Spring Boot 和 @DataJpaTest 測試 JPA 查詢的文章。
@DataJdbcTest
Spring Data JDBC 是 Spring Data 系列的另一個成員。 如果我們正在使用這個項目並且想要測試持久層,那么我們可以使用 @DataJdbcTest
注解 。@DataJdbcTest
會自動為我們配置在我們的項目中定義的嵌入式測試數據庫和 JDBC 存儲庫。
另一個類似的項目是 Spring JDBC,它為我們提供了 JdbcTemplate
對象來執行直接查詢。@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 創建 ApplicationContext
@SpringBootTest
在默認情況下開始在測試類的當前包中搜索,然后在包結構中向上搜索,尋找用 @SpringBootConfiguration
注解的類,然后從中讀取配置以創建應用程序上下文。這個類通常是我們的主要應用程序類,因為 @SpringBootApplication
注解包括 @SpringBootConfiguration
注解。然后,它會創建一個與在生產環境中啟動的應用程序上下文非常相似的應用程序上下文。
我們可以通過許多不同的方式自定義此應用程序上下文,如下一節所述。
因為我們有一個完整的應用程序上下文,包括 web 控制器、Spring 數據存儲庫和數據源,@SpringBootTest
對於貫穿應用程序所有層的集成測試非常方便:
@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 實例添加到應用程序上下文中。
我們使用這個 MockMvc
對象向我們的應用程序執行 POST 請求並驗證它是否按預期響應。
然后,我們使用應用程序上下文中的 UserRepository
來驗證請求是否導致數據庫狀態發生預期的變化。
自定義應用程序上下文
我們可以有很多種方法來自定義 @SpringBootTest
創建的應用程序上下文。讓我們看看我們有哪些選擇。
自定義應用上下文時的注意事項
應用程序上下文的每個自定義都是使其與在生產設置中啟動的“真實”應用程序上下文不同的另一件事。因此,為了使我們的測試盡可能接近生產,我們應該只定制讓測試運行真正需要的東西!
添加自動配置
在上面,我們已經看到了自動配置的作用:
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
...
}
還有很多其他可用的自動配置,每個都可以將其他 bean 添加到應用程序上下文中。以下是文檔中其他一些有用的內容:
@AutoConfigureWebTestClient
:將WebTestClient
添加到測試應用程序上下文。它允許我們測試服務器端點。@AutoConfigureTestDatabase
:這允許我們針對真實數據庫而不是嵌入式數據庫運行測試。@RestClientTest
:當我們想要測試我們的RestTemplate
時它會派上用場。 它自動配置所需的組件以及一個MockRestServiceServer
對象,該對象幫助我們模擬來自RestTemplate
調用的請求的響應。@JsonTest
:自動配置 JSON 映射器和類,例如JacksonTester
或GsonTester
。使用這些我們可以驗證我們的 JSON 序列化/反序列化是否正常工作。
設置自定義配置屬性
通常,在測試中需要將一些配置屬性設置為與生產設置中的值不同的值:
@SpringBootTest(properties = "foo=bar")
class SpringBootPropertiesTest {
@Value("${foo}")
String foo;
@Test
void test(){
assertThat(foo).isEqualTo("bar");
}
}
如果屬性 foo
存在於默認設置中,它將被此測試的值 bar
覆蓋。
使用 @ActiveProfiles 外部化屬性
如果我們的許多測試需要相同的屬性集,我們可以創建一個配置文件 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 設置自定義屬性
另一種定制整個屬性集的方法是使用 @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
文件中的所有屬性都加載到應用程序上下文中。@TestPropertySource
還可以 配置更多。
使用 @MockBean 注入模擬
如果我們只想測試應用程序的某個部分而不是從傳入請求到數據庫的整個路徑,我們可以使用 @MockBean
替換應用程序上下文中的某些 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
方法,我們指定了此模擬的預期行為,以測試使用此存儲庫的類。
您可以在我關於模擬的文章中閱讀有關 @MockBean
注解的更多信息。
使用 @Import 添加 Bean
如果某些 bean 未包含在默認應用程序上下文中,但我們在測試中需要它們,我們可以使用 @Import
注解導入它們:
package other.namespace;
@Component
public class Foo {
}
@SpringBootTest
@Import(other.namespace.Foo.class)
class SpringBootImportTest {
@Autowired
Foo foo;
@Test
void test() {
assertThat(foo).isNotNull();
}
}
默認情況下,Spring Boot 應用程序包含它在其包和子包中找到的所有組件,因此通常只有在我們想要包含其他包中的 bean 時才需要這樣做。
使用 @TestConfiguration 覆蓋 Bean
使用 @TestConfiguration
,我們不僅可以包含測試所需的其他 bean,還可以覆蓋應用程序中已經定義的 bean。在我們關於使用 @TestConfiguration
進行測試的文章中閱讀更多相關信息。
創建自定義 @SpringBootApplication
我們甚至可以創建一個完整的自定義 Spring Boot 應用程序來啟動測試。如果這個應用程序類與真正的應用程序類在同一個包中,但是在測試源而不是生產源中,@SpringBootTest
會在實際應用程序類之前找到它,並從這個應用程序加載應用程序上下文。
或者,我們可以告訴 Spring Boot 使用哪個應用程序類來創建應用程序上下文:
@SpringBootTest(classes = CustomApplication.class)
class CustomApplicationTest {
}
但是,在執行此操作時,我們正在測試可能與生產環境完全不同的應用程序上下文,因此僅當無法在測試環境中啟動生產應用程序時,這才應該是最后的手段。但是,通常有更好的方法,例如使真實的應用程序上下文可配置以排除不會在測試環境中啟動的 bean。讓我們看一個例子。
假設我們在應用程序類上使用 @EnableScheduling
注解。每次啟動應用程序上下文時(即使在測試中),所有 @Scheduled
作業都將啟動,並且可能與我們的測試沖突。 我們通常不希望作業在測試中運行,因此我們可以創建第二個沒有 @EnabledScheduling
注釋的應用程序類,並在測試中使用它。但是,更好的解決方案是創建一個可以使用屬性切換的配置類:
@Configuration
@EnableScheduling
@ConditionalOnProperty(
name = "io.reflectoring.scheduling.enabled",
havingValue = "true",
matchIfMissing = true)
public class SchedulingConfiguration {
}
我們已將 @EnableScheduling
注解從我們的應用程序類移到這個特殊的配置類。將屬性 io.reflectoring.scheduling.enabled
設置為 false
將導致此類不會作為應用程序上下文的一部分加載:
@SpringBootTest(properties = "io.reflectoring.scheduling.enabled=false")
class SchedulingTest {
@Autowired(required = false)
private SchedulingConfiguration schedulingConfiguration;
@Test
void test() {
assertThat(schedulingConfiguration).isNull();
}
}
我們現在已經成功地停用了測試中的預定作業。屬性 io.reflectoring.scheduling.enabled
可以通過上述任何方式指定。
為什么我的集成測試這么慢?
包含大量 @SpringBootTest
注釋測試的代碼庫可能需要相當長的時間才能運行。Spring 的測試支持 足夠智能,只創建一次應用上下文並在后續測試中重復使用,但是如果不同的測試需要不同的應用上下文,它仍然會為每個測試創建一個單獨的上下文,這需要一些時間來完成每個測試。
上面描述的所有自定義選項都會導致 Spring 創建一個新的應用程序上下文。因此,我們可能希望創建一個配置並將其用於所有測試,以便可以重用應用程序上下文。
如果您對測試花費在設置和 Spring 應用程序上下文上的時間感興趣,您可能需要查看 JUnit Insights,它可以包含在 Gradle 或 Maven 構建中,以生成關於 JUnit 5 如何花費時間的很好的報告。
結論
@SpringBootTest
是一種為測試設置應用程序上下文的非常方便的方法,它非常接近我們將在生產中使用的上下文。有很多選項可以自定義此應用程序上下文,但應謹慎使用它們,因為我們希望我們的測試盡可能接近生產運行。
如果我們想在整個應用程序中進行測試,@SpringBootTest
會帶來最大的價值。為了僅測試應用程序的某些切片或層,我們還有其他選項可用。
本文中使用的示例代碼可在 github 上找到。