為何整合 HtmlUnit?
最明顯的問題是“我為什麼需要它?”答案最好透過探索一個非常基本的示例應用來找到。假設你有一個支援對 Message
物件進行 CRUD 操作的 Spring MVC Web 應用。該應用還支援對所有訊息進行分頁。你會如何測試它?
使用 Spring MVC Test,我們可以輕鬆地測試是否能夠建立 Message
,如下所示
-
Java
-
Kotlin
MockHttpServletRequestBuilder createMessage = post("/messages/")
.param("summary", "Spring Rocks")
.param("text", "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));
@Test
fun test() {
mockMvc.post("/messages/") {
param("summary", "Spring Rocks")
param("text", "In case you didn't know, Spring Rocks!")
}.andExpect {
status().is3xxRedirection()
redirectedUrl("/messages/123")
}
}
如果我們想測試建立訊息的表單檢視呢?例如,假設我們的表單看起來像下面的片段
<form id="messageForm" action="/messages/" method="post">
<div class="pull-right"><a href="/messages/">Messages</a></div>
<label for="summary">Summary</label>
<input type="text" class="required" id="summary" name="summary" value="" />
<label for="text">Message</label>
<textarea id="text" name="text"></textarea>
<div class="form-actions">
<input type="submit" value="Create" />
</div>
</form>
我們如何確保我們的表單生成了建立新訊息的正確請求?一個簡單的嘗試可能如下所示
-
Java
-
Kotlin
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='summary']").exists())
.andExpect(xpath("//textarea[@name='text']").exists());
mockMvc.get("/messages/form").andExpect {
xpath("//input[@name='summary']") { exists() }
xpath("//textarea[@name='text']") { exists() }
}
這個測試有一些明顯的缺點。如果我們更新控制器使用引數 message
而不是 text
,我們的表單測試仍然會透過,儘管 HTML 表單與控制器不同步。為了解決這個問題,我們可以結合我們的兩個測試,如下所示
-
Java
-
Kotlin
String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());
MockHttpServletRequestBuilder createMessage = post("/messages/")
.param(summaryParamName, "Spring Rocks")
.param(textParamName, "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));
val summaryParamName = "summary";
val textParamName = "text";
mockMvc.get("/messages/form").andExpect {
xpath("//input[@name='$summaryParamName']") { exists() }
xpath("//textarea[@name='$textParamName']") { exists() }
}
mockMvc.post("/messages/") {
param(summaryParamName, "Spring Rocks")
param(textParamName, "In case you didn't know, Spring Rocks!")
}.andExpect {
status().is3xxRedirection()
redirectedUrl("/messages/123")
}
這會降低我們的測試錯誤透過的風險,但仍然存在一些問題
-
如果我們的頁面上有多個表單怎麼辦?誠然,我們可以更新 XPath 表示式,但當我們考慮更多因素時,它們會變得更加複雜:欄位型別是否正確?欄位是否啟用?等等。
-
另一個問題是,我們做了雙倍於預期的工作。我們必須先驗證檢視,然後使用剛剛驗證過的相同引數提交檢視。理想情況下,這可以一次完成。
-
最後,有些事情我們仍然無法考慮進去。例如,如果表單有我們希望測試的 JavaScript 驗證怎麼辦?
總體問題是,測試一個網頁不僅僅涉及單一互動。相反,它是使用者與網頁互動以及該網頁與其他資源互動的組合。例如,表單檢視的結果被用作使用者建立訊息的輸入。此外,我們的表單檢視可能還會使用影響頁面行為的其他資源,例如 JavaScript 驗證。
整合測試能解決問題嗎?
為了解決前面提到的問題,我們可以執行端到端整合測試,但這有一些缺點。考慮測試允許我們對訊息進行分頁的檢視。我們可能需要以下測試
-
當訊息為空時,我們的頁面是否會向用戶顯示通知,表明沒有結果?
-
我們的頁面是否正確顯示單條訊息?
-
我們的頁面是否正確支援分頁?
為了設定這些測試,我們需要確保資料庫包含正確的訊息。這帶來了一些額外的挑戰
-
確保資料庫中有正確的訊息可能會很繁瑣。(考慮外部索引鍵約束。)
-
測試可能會變慢,因為每個測試都需要確保資料庫處於正確狀態。
-
由於我們的資料庫需要處於特定狀態,我們無法並行執行測試。
-
對自動生成的 ID、時間戳等專案執行斷言可能會很困難。
這些挑戰並不意味著我們應該完全放棄端到端整合測試。相反,我們可以透過重構詳細測試來使用模擬服務,這些服務執行更快、更可靠且沒有副作用,從而減少端到端整合測試的數量。然後,我們可以實現少量真實的端到端整合測試,這些測試驗證簡單的流程以確保一切正常協同工作。
HtmlUnit 整合選項
當你想將 MockMvc 與 HtmlUnit 整合時,有多種選項
-
MockMvc 和 HtmlUnit: 如果你想使用原生的 HtmlUnit 庫,請選擇此選項。
-
MockMvc 和 WebDriver: 使用此選項可以簡化開發,並在整合測試和端到端測試之間複用程式碼。
-
MockMvc 和 Geb: 如果你想使用 Groovy 進行測試、簡化開發並在整合測試和端到端測試之間複用程式碼,請選擇此選項。