1. 概述
集成測試在應用程序開發週期中發揮着重要作用,通過驗證系統的端到端行為。
在本教程中,我們將學習如何利用 Spring MVC 測試框架編寫和運行集成測試,這些測試將驗證控制器,而無需顯式啓動 Servlet 容器。
2. 準備工作
我們需要幾個 Maven 依賴項才能運行本文檔中使用的集成測試。首先,我們需要最新版本的 junit-jupiter-引擎、junit-jupiter-api 和 Spring Test 依賴項:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.13</version>
<scope>test</scope>
</dependency>3. Spring MVC 測試配置
現在,讓我們看看如何配置和運行 Spring 啓用的測試。
3.1. 使用 JUnit 5 啓用 Spring
JUnit 5 通過定義一個擴展接口,使類能夠與 JUnit 測試集成。
我們可以通過在測試類中添加 <em>@ExtendWith</em> 標註,並指定要加載的擴展類來啓用此擴展。為了運行 Spring 測試,我們使用SpringExtension.class。`
我們還需要 <strong><em>@ContextConfiguration</em> 標註來加載上下文配置並啓動測試所使用的上下文。
讓我們來看一下:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { ApplicationConfig.class })
@WebAppConfiguration
public class GreetControllerIntegrationTest {
....
}請注意,在 @ContextConfiguration 中,我們提供了 ApplicationConfig.class 配置類,該類加載了我們針對此特定測試所需的配置。
我們將使用 Java 配置類來指定上下文配置。 類似地,我們也可以使用基於 XML 的配置:
@ContextConfiguration(locations={""})最後,我們還會使用 @WebAppConfiguration 註解標記測試,這將會加載 Web 應用程序上下文。
默認情況下,它會在 src/main/webapp 路徑下查找根 Web 應用程序。可以通過傳遞 value 屬性來覆蓋此位置:
@WebAppConfiguration(value = "")3.2 WebApplicationContext 對象
WebApplicationContext 提供了一個 Web 應用程序的配置。它將所有應用程序的 Bean 和控制器加載到上下文中。
現在,我們將能夠將 Web 應用程序上下文直接注入到測試中:
@Autowired
private WebApplicationContext webApplicationContext;3.3. 模擬 Web Context Bean
`MockMvc 提供對 Spring MVC 測試的支持。它封裝了所有 Web 應用程序 Bean,並使它們可供測試使用。
讓我們看看如何使用它:
private MockMvc mockMvc;
@BeforeEach
public void setup() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build();
}我們將初始化 mockMvc 對象,在帶有 @BeforeEach 註解的方法中,這樣就不需要在每個測試中重複初始化它。
3.4. 驗證測試配置
讓我們驗證我們是否正確加載了 WebApplicationContext 對象(webApplicationContext)。 此外,我們還將檢查是否已正確附加了正確的 servletContext。
@Test
public void givenWac_whenServletContext_thenItProvidesGreetController() {
ServletContext servletContext = webApplicationContext.getServletContext();
assertNotNull(servletContext);
assertTrue(servletContext instanceof MockServletContext);
assertNotNull(webApplicationContext.getBean("greetController"));
}請注意,我們還檢查是否存在一個 GreetController.java bean 在 Web 上下文中。 這確保了 Spring bean 的正確加載。 在此,集成測試的設置已完成。 現在,我們將看看如何使用 MockMvc 對象測試資源方法。
4. 編寫集成測試
在本節中,我們將介紹通過測試框架可用的基本操作。
我們將探討如何使用路徑變量和參數發送請求。我們還將提供一些示例,展示如何斷言正確的視圖名稱已解析,或者響應體符合預期。
下面的代碼片段使用了來自 MockMvcRequestBuilders 和 MockMvcResultMatchers 類中的靜態導入。
4.1. 驗證視圖名稱
我們可以從我們的測試中調用 /homePage</em/> 終點,如下所示:
http://localhost:8080/spring-mvc-test/或
http://localhost:8080/spring-mvc-test/homePage首先,讓我們來看一下測試代碼:
@Test
public void givenHomePageURI_whenMockMVC_thenReturnsIndexJSPViewName() {
this.mockMvc.perform(get("/homePage")).andDo(print())
.andExpect(view().name("index"));
}我們來分解一下:
- perform() 方法將調用 GET 請求方法,並返回 ResultActions。我們可以利用這個結果對響應進行斷言,例如其內容、HTTP 狀態碼或頭部信息。
- andDo(print()) 將打印請求和響應。這在出現錯誤時可以提供詳細的視圖。
- andExpect() 將期望提供的參數。在本例中,我們期望通過 MockMvcResultMatchers.view() 返回 "index"。
4.2. 驗證響應體
我們將從我們的測試中調用 greet</em/> 端點,如下所示:
http://localhost:8080/spring-mvc-test/greet預期的輸出將是:
{
"id": 1,
"message": "Hello World!!!"
}讓我們來看測試代碼:
@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() {
MvcResult mvcResult = this.mockMvc.perform(get("/greet"))
.andDo(print()).andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Hello World!!!"))
.andReturn();
assertEquals("application/json;charset=UTF-8", mvcResult.getResponse().getContentType());
}讓我們來詳細瞭解一下發生了什麼:
- andExpect(MockMvcResultMatchers.status().isOk()) 將驗證響應的 HTTP 狀態碼是否為 (200) Ok。 這確保了請求已成功執行。
- andExpect(MockMvcResultMatchers.jsonPath(“$.message”).value(“Hello World!!!”)) 將驗證響應內容是否與參數 "Hello World!!!” 匹配。 這裏我們使用了 jsonPath,它提取響應內容並提供請求的值。
- andReturn() 將返回 MvcResult 對象,該對象用於我們無法直接通過庫驗證的情況。 在這種情況下,我們添加了 assertEquals 以匹配從 MvcResult 對象中提取的響應內容的內容類型。
3.4 使用路徑變量發送 GET 請求
我們將從我們的測試中調用 /greetWithPathVariable/{name}</em/> 終點,如下所示:
http://localhost:8080/spring-mvc-test/greetWithPathVariable/John預期的輸出將是:
{
"id": 1,
"message": "Hello World John!!!"
}讓我們來看測試代碼:
@Test
public void givenGreetURIWithPathVariable_whenMockMVC_thenResponseOK() {
this.mockMvc
.perform(get("/greetWithPathVariable/{name}", "John"))
.andDo(print()).andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.message").value("Hello World John!!!"));
}MockMvcRequestBuilders.get(“/greetWithPathVariable/{name}”, “John”) 將會發送一個請求,請求 URL 為 “/greetWithPathVariable/John”。
這樣可以提高可讀性,並更容易理解 URL 中哪些參數是動態設置的。請注意,我們可以傳遞任意數量的路徑參數。
4.4. 使用查詢參數發送 GET 請求
我們將從我們的測試中調用 /greetWithQueryVariable?name={name}</em/> 端點:
http://localhost:8080/spring-mvc-test/greetWithQueryVariable?name=John%20Doe在這種情況下,預期的輸出將是:
{
"id": 1,
"message": "Hello World John Doe!!!"
}現在,讓我們來看一下測試代碼:
@Test
public void givenGreetURIWithQueryParameter_whenMockMVC_thenResponseOK() {
this.mockMvc.perform(get("/greetWithQueryVariable")
.param("name", "John Doe")).andDo(print()).andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.message").value("Hello World John Doe!!!"));
}param(“name”, “John Doe”) 將會在 GET 請求中添加查詢參數. 類似於 “/greetWithQueryVariable?name=John%20Doe.“
查詢參數也可以使用 URI 模板風格實現:
this.mockMvc.perform(
get("/greetWithQueryVariable?name={name}", "John Doe"));4.5. 發送 POST 請求
我們將從我們的測試中調用 greetWithPost</em/> 端點,如下所示:
http://localhost:8080/spring-mvc-test/greetWithPost我們應該獲得以下輸出:
{
"id": 1,
"message": "Hello World!!!"
}我們的測試代碼如下:
@Test
public void givenGreetURIWithPost_whenMockMVC_thenVerifyResponse() {
this.mockMvc.perform(post("/greetWithPost")).andDo(print())
.andExpect(status().isOk()).andExpect(content()
.contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.message").value("Hello World!!!"));
}MockMvcRequestBuilders.post(“/greetWithPost”) 將發送 POST 請求。 我們可以以與之前類似的方式設置路徑變量和查詢參數,而表單數據只能通過 param() 方法設置,類似於查詢參數,如下所示:
http://localhost:8080/spring-mvc-test/greetWithPostAndFormData然後數據將會是:
id=1;name=John%20Doe因此,我們應該得到:
{
"id": 1,
"message": "Hello World John Doe!!!"
}讓我們看看我們的測試:
@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/greet"))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Hello World!!!"))
.andReturn();
assertEquals("application/json;charset=UTF-8", mvcResult.getResponse().getContentType());
}在上面的代碼片段中,我們添加了兩個參數:id 為“1”,name 為“John Doe”。
5. MockMvc 的侷限性
MockMvc 提供了一個優雅且易於使用的 API,用於調用 Web 端點,並在同一時間對其響應進行檢查和斷言。儘管它有很多優點,但它也有一些侷限性。
首先,它使用的是 DispatcherServlet 的子類來處理測試請求。更具體地説,TestDispatcherServlet 負責調用控制器並執行所有熟悉的 Spring 魔法。
MockMvc 類內部使用 包裝 了這個 TestDispatcherServlet。因此,每次我們使用 perform() 方法發送請求時,MockMvc 將直接使用底層的 TestDispatcherServlet。因此,不會建立任何實際的網絡連接,並且因此,使用 MockMvc 時,我們無法測試整個網絡堆棧。
此外,由於 Spring 準備了一個虛假的 Web 應用上下文來模擬 HTTP 請求和響應,因此它可能不支持完整版的 Spring 應用的所有功能。
例如,這個模擬設置不支持 HTTP 重定向。這在最初可能看起來不太重要。但是,Spring Boot 通過將當前請求重定向到 /error 端點來處理某些錯誤。因此,如果使用 MockMvc,我們可能無法測試某些 API 故障。
作為 MockMvc 的替代方案,我們可以設置一個更真實的應用程序上下文,然後使用 RestTemplate,甚至 REST-assured 來測試我們的應用程序。
例如,這在 Spring Boot 中很容易實現:
@SpringBootTest(webEnvironment = DEFINED_PORT)
public class GreetControllerRealIntegrationTest {
@Before
public void setUp() {
RestAssured.port = DEFAULT_PORT;
}
@Test
public void givenGreetURI_whenSendingReq_thenVerifyResponse() {
given().get("/greet")
.then()
.statusCode(200);
}
}在這裏,我們甚至不需要添加 @ExtendWith(SpringExtension.class)。
這樣一來,每個測試都會向監聽在隨機 TCP 端口上的應用程序發出真實的 HTTP 請求。
6. 結論
在本文中,我們實施了幾項簡單的 Spring 支持集成測試。
我們還考察了 WebApplicationContext 和 MockMvc 對象創建,這在調用應用程序端點的過程中起着重要作用。
進一步地,我們討論瞭如何通過參數傳遞的變化發送 GET 和 POST 請求,以及如何驗證 HTTP 響應狀態、標題和內容。
然後,我們評估了 MockMvc 的一些侷限性。瞭解這些侷限性可以幫助我們做出明智的決策,關於我們如何實現測試。