【Spring Boot】第9課-MockMvc 整合測試(一)

在前面的文章,從建立 Controller、Service,到串接資料庫,都是使用 Postman 來觀察程式的執行結果。然而隨著專案規模擴大,像這樣手動測試會非常麻煩。本文就讓我們來撰寫整合測試,利用程式來測試現有的功能。

本文的練習用專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch8

一、測試的目的

測試的目的是確認功能可以正常運作,例如先前對後端 API 的測試是透過 Postman。然而逐一測試會很費時,因此需要寫程式來自動化。

每當開發新功能、修復 Bug 後,我們通常需要針對該次的改動撰寫測試程式,並與舊有的測試一起執行。除了證明該次交付的程式可以正常運作或能解決問題,也要確保不會影響現有的功能。

本文所要講解的是「整合測試」(integration test),並非「單元測試」(unit test)。整合測試會依賴於外部的服務,比方說我們串接的 MongoDB 資料庫就是一種外部服務,測試時一樣會串接。而單元測試會將外部服務用「模擬物件」(Mock)的方式來替代,但不在本文的範疇。


二、建立測試類別

本文會用到 Spring Boot 提供的「MockMvc」元件。它能針對當前專案模擬出發送 HTTP 請求的動作,並取得狀態碼、回應標頭(response header)與主體(response body)等結果,跟 Postman 很像。此外也可以檢查回應中的內容是否如開發者所預期,例如有幾筆資料、某個 JSON 欄位值為多少。

為了撰寫整合測試,請在 pom.xml 檔案匯入測試函式庫。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>

首先在專案的 src\test\java 路徑中建立一個測試類別叫做 ProductTest,並且 package 要跟 src\main\java 相同(筆者的範例專案是 com.vincent.demo)。它會存放用來測試 Product 功能的程式。

接著在類別加上三個標記。@RunWith@SpringBootTest 是定義測試程式要在 Spring Boot 的環境下執行。@AutoConfigureMockMvc 代表測試開始時會在元件容器中建立 MockMvc 元件,請讀者注入進來。

package com.vincent.demo;

// 略過 import

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ProductTest {
@Autowired
private MockMvc mockMvc;

}

測試程式和原本業務邏輯的程式一樣,也有自己的環境參數配置檔。因此請在 src\test 路徑下新增「resources」資料夾,建立「application.properties」檔案。並添加測試程式專用的資料庫專屬位址。請不要與開發中程式使用相同的資料庫,避免互相影響。 

spring.data.mongodb.uri=mongodb://localhost:27017/testing

筆者的公司則設定成每位同事都使用自己的資料庫,如下:

# ${USERNAME} 會自動帶入電腦的使用者名稱
spring.data.mongodb.uri=mongodb://192.168.xxx.xxx:27017/${USERNAME}

三、撰寫測試案例

測試類別準備好後,就能撰寫測試程式了。本文以新增產品的情境為例,請宣告一個方法,加上 @Test 標記,代表這是要被執行的測試程式,其正式名稱為「測試案例」(test case)。

public class ProductTest {
@Autowired
private MockMvc mockMvc;

@Test
public void testCreateProduct() throws Exception {

}
}

為了進行測試,下面會準備一些有關請求的資料,包含請求標頭與主體、HTTP 方法及目標 API。其實在操作 Postman 時都認識過了,只是這邊改為透過程式碼來完成。

使用 Postman 時會在「Body」頁籤選擇「raw」與「JSON」,再輸入 JSON 字串作為請求主體,因此在「Headers」頁籤能發現「Content-Type」的資料。

在測試程式中同樣會準備這些資料,我們以 HttpHeadersJSONObject 提供。

@Test
public void testCreateProduct() throws Exception {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

JSONObject request = new JSONObject()
.put("name", "Harry Potter")
.put("price", 450);
// TODO
}

上面的 HttpHeaders 物件是用來存放請求標頭的資料,此處存放了 Content-Type: application/json 這項資料。而 HttpHeadersMediaType 類別中正好有這些字串常數,於是筆者便拿來利用。

綜合以上資料,我們使用 MockMvcRequestBuilders 類別,透過 builder 的方式來建立請求。最後得到 MockHttpServletRequestBuilder 物件,它是 RequestBuilder 介面的物件。
@Test
public void testCreateProduct() throws Exception {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

JSONObject request = new JSONObject()
.put("name", "Harry Potter")
.put("price", 450);

RequestBuilder requestBuilder =
MockMvcRequestBuilders
.post("/products")
.headers(httpHeaders)
.content(request.toString());
// TODO
}

上面用了三個方法。post 方法代表發送 POST 請求,參數需傳入 API 路徑。headers 方法可附加請求標頭。content 方法可加入請求主體,此處傳入 JSON 字串作為參數。


四、發出請求與驗證

請求資料建立完成後,就能透過 MockMvc 元件來發出模擬請求。只要透過 perform 方法,傳入上一節建立的請求資料即可。perform 方法的參數接受 RequestBuilder 介面的物件。

@Test
public void testCreateProduct() throws Exception {
// 其餘略過

mockMvc.perform(requestBuilder)
.andDo(print())
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").hasJsonPath())
.andExpect(jsonPath("$.name").value(request.getString("name")))
.andExpect(jsonPath("$.price").value(request.getInt("price")))
.andExpect(header().exists(HttpHeaders.LOCATION))
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE));
}

請求發出後,隨即透過 andExpect 方法進行回應資料的驗證。以下介紹在範例中使用到的驗證方式。

  1. status().isCreated():驗證 HTTP 狀態碼應為201。讀者可自行探索其他狀態,如 isOkisNotFound 等。或透過 is 方法直接傳入狀態碼。
  2. jsonPath():獲取指定 JSON 欄位的值。以「$」符號開始,使用「.」符號前往下一層的路徑。
  3. hasJsonPath():驗證某個 JSON 欄位存在。
  4. value():驗證某個 JSON 欄位值為何。
  5. header().exists():驗證回應標頭中的某欄位存在。
  6. header().string():驗證回應標頭中的某欄位值為何。

使用 andDO(print()) 方法,能將測試程式的請求與回應詳情印在 Console 視窗。

這樣就寫好一個測試程式了。在程式碼左邊的行數區塊,找到綠色三角形,按下去便能執行。下方的 Console 視窗會顯示一些資料,例如回應內容。而左邊的綠色勾勾代表測試通過,橘色代表驗證失敗,紅色代表發生例外。

若一個測試案例中,遇到某一個驗證未通過,那程式就不會繼續往下執行。讀者應根據 Console 視窗內的訊息,找出未通過的原因。可能是業務邏輯或測試程式有問題。

而若要執行多個測試案例,請按下測試類別名稱旁邊的綠色三角形。或是在 IntelliJ 最左邊的專案檢視視窗,選取一到多個測試類別,按右鍵一次執行。

本文的完成範例:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch9

上一篇:【Spring Boot】第8.6課-使用 JPA 建立多對多關聯,並配置中間表

下一篇:【Spring Boot】第10課-MockMvc 整合測試(二)

留言