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

第9課初次接觸了整合測試,並撰寫新增產品的測試案例。本文會介紹如何在測試案例的執行前後安排固定的工作。並針對其餘的 GET、PUT、DELETE 等 API 進行測試。另外也會取出 MockMvc 執行結果中的資料,搭配 JUnit 的斷言方法(Assertion)來實作另一種驗證方式。

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

一、測試的前後處理

我們可以在各個測試案例的執行前後,安排固定的工作,例如資料的準備與清除。尤其測試案例不應該被其他殘留資料干擾,所以在每個測試案例執行後,理應將資料庫清空。所以執行前,資料庫都是「乾淨」的。

為了進行前後處理,筆者在下面撰寫兩個方法,在測試前初始化 HttpHeader 物件,並在測試後清除資料庫。它們分別冠上了 @Before@After 標記,且需宣告為 public,如此才會分別在測試前與後自動執行。

public class ProductTest {
private HttpHeaders httpHeaders;

@Autowired
private ProductRepository productRepository;

@Before
public void init() {
httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}

@After
public void clear() {
productRepository.deleteAll();
}

// 其餘略過
}

若測試被手動中止,會導致沒有執行到清除資料的程式,下次的測試可能會被殘留資料影響。因此讀者也可考慮改成在測試前就先清除資料庫。


二、撰寫更多測試案例

接下來讓我們完成其他測試案例。為了有產品可以操作,因此筆者撰寫了 createProduct 方法來專門產生資料。

private Product createProduct(String name, int price) {
Product product = new Product();
product.setName(name);
product.setPrice(price);

return product;
}

以下的範例測試是發送取得單一產品的 GET 請求。為了讓程式碼簡化一些,筆者直接將一連串動作在 perform 方法的參數中完成。

@Test
public void testGetProduct() throws Exception {
Product product = createProduct("Economics", 450);
productRepository.insert(product);

mockMvc.perform(get("/products/" + product.getId())
.headers(httpHeaders))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(product.getId()))
.andExpect(jsonPath("$.name").value(product.getName()))
.andExpect(jsonPath("$.price").value(product.getPrice()));
}

以下的範例測試是發送更新產品的 PUT 請求。做法會像上一篇的 POST 請求那樣,使用 content 方法加入請求主體(request body)。

@Test
public void testReplaceProduct() throws Exception {
Product product = createProduct("Economics", 450);
productRepository.insert(product);

JSONObject request = new JSONObject()
.put("name", "Macroeconomics")
.put("price", 550);

mockMvc.perform(put("/products/" + product.getId())
.headers(httpHeaders)
.content(request.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(product.getId()))
.andExpect(jsonPath("$.name").value(request.getString("name")))
.andExpect(jsonPath("$.price").value(request.getInt("price")));
}

以下的範例測試是發送刪除產品的 DELETE 請求。最後會檢查資料庫,確認產品已經不存在,拋出預期的例外。

@Test(expected = RuntimeException.class)
public void testDeleteProduct() throws Exception {
Product product = createProduct("Economics", 450);
productRepository.insert(product);

mockMvc.perform(delete("/products/" + product.getId())
.headers(httpHeaders))
.andExpect(status().isNoContent());

productRepository.findById(product.getId())
.orElseThrow(RuntimeException::new);
}

三、在 MockMvc 攜帶查詢字串

筆者在第4課介紹了網址上的參數,也就是查詢字串(query string)。本節會以查詢多個產品的 GET 請求為例子,在 MockMvc 發送的請求中加入參數。使用 param 方法可在請求中添加查詢字串。以下的範例測試是搜尋名稱包含「Manage」的產品,並依照價格遞增排序。

@Test
public void testSearchProductsSortByPriceAsc() throws Exception {
Product p1 = createProduct("Operation Management", 350);
Product p2 = createProduct("Marketing Management", 200);
Product p3 = createProduct("Human Resource Management", 420);
Product p4 = createProduct("Finance Management", 400);
Product p5 = createProduct("Enterprise Resource Planning", 440);
productRepository.insert(Arrays.asList(p1, p2, p3, p4, p5));

mockMvc.perform(get("/products")
.headers(httpHeaders)
.param("keyword", "Manage")
.param("orderBy", "price")
.param("sortRule", "asc"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(4)))
.andExpect(jsonPath("$[0].id").value(p2.getId()))
.andExpect(jsonPath("$[1].id").value(p1.getId()))
.andExpect(jsonPath("$[2].id").value(p4.getId()))
.andExpect(jsonPath("$[3].id").value(p3.getId()));
}

上面是根據 ProductService 的實作,給予「keyword」、「orderBy」和「sortRule」的參數,來添加關鍵字與排序方式。此 API 會回傳多個產品,因此驗證的 JSON 欄位路徑(jsonPath),會以「[]」符號表示陣列索引。其他查詢情境的測試案例,讀者可以自行練習。


四、取出 MockMvc 的執行結果

先前的範例都是使用 MockMvcandExpect 方法,驗證回應中的資料,但這樣的做法有時滿足不了測試需求。比方說第三節的關鍵字搜尋,如果不進行排序,其實我們不應該假設產品的順序就是存入資料庫的順序,所以不適合直接用陣列索引驗證。

這一節我們來改寫前述搜尋產品的測試。這會取出 MockMvc 的執行結果,並搭配「JUnit」函式庫提供的斷言(Assertion)方法來驗證資料。

透過 MockMvcandReturn 方法,能得到 MvcResult 物件。再透過它的 getResponse 方法,得到 MockHttpServletResponse 物件。該物件會有一些資料可以取出來,讓我們做其他使用。

下面筆者對 testSearchProductsSortByPriceAsc 這個測試案例改寫。請在發出請求後,取得 MockHttpServletResponse 物件,透過它的 getContentAsString 方法取出回應主體的 JSON 字串,再轉為 JSONArray

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

MvcResult result = mockMvc.perform(get("/products")
.headers(httpHeaders)
.param("keyword", "Manage")
.param("orderBy", "price")
.param("sortRule", "asc"))
.andReturn();

MockHttpServletResponse mockHttpResponse = result.getResponse();
String responseJSONStr = mockHttpResponse.getContentAsString();
JSONArray productJSONArray = new JSONArray(responseJSONStr);
// TODO
}

由於 id 能夠當作產品的唯一識別,我們只要驗證回應主體中都是預期的 id 即可。所以接著將 JSONArray 中的產品 id 給收集起來。

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

JSONArray productJSONArray = new JSONArray(responseJSONStr);

List<String> productIds = new ArrayList<>();
for (int i = 0; i < productJSONArray.length(); i++) {
JSONObject productJSON = productJSONArray.getJSONObject(i);
productIds.add(productJSON.getString("id"));
}
// TODO
}

收集完產品 id 後,下面使用斷言方法來驗證資料。首先驗證應該要有4個產品被查詢到。接著根據預期的排序結果,驗證第1~4個產品的 id。筆者在最後還取出了 HTTP 狀態碼與回應標頭中的 Content-Type 進行驗證。

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

Assert.assertEquals(4, productIds.size());
Assert.assertEquals(p2.getId(), productIds.get(0));
Assert.assertEquals(p1.getId(), productIds.get(1));
Assert.assertEquals(p4.getId(), productIds.get(2));
Assert.assertEquals(p3.getId(), productIds.get(3));

Assert.assertEquals(HttpStatus.OK.value(), mockHttpResponse.getStatus());
Assert.assertEquals(MediaType.APPLICATION_JSON_VALUE,
mockHttpResponse.getHeader(HttpHeaders.CONTENT_TYPE));
}

若測試案例沒有指定要排序,驗證可以用如下的做法。

Assert.assertTrue(productIds.contains(p1.getId()));
Assert.assertTrue(productIds.contains(p2.getId()));
Assert.assertTrue(productIds.contains(p3.getId()));
Assert.assertTrue(productIds.contains(p4.getId()));

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

上一篇:【Spring Boot】第9課-MockMvc 自動化測試(一)

下一篇:【Spring Boot】第11課-驗證請求資料

留言