為何整合 HtmlUnit?
最明顯的問題是“我為什麼需要這個?”答案最好透過探索一個非常基本的示例應用程式來找到。假設您有一個 Spring MVC Web 應用程式,它支援對 Message 物件進行 CRUD 操作。該應用程式還支援分頁瀏覽所有訊息。您將如何測試它?
透過 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、時間戳等專案進行斷言可能很困難。
這些挑戰並不意味著我們應該完全放棄端到端整合測試。相反,我們可以透過重構我們的詳細測試以使用執行更快、更可靠且沒有副作用的模擬服務來減少端到端整合測試的數量。然後,我們可以實施少量真正的端到端整合測試,驗證簡單的 workflow,以確保所有內容都正常協同工作。
HtmlUnit 整合選項
當您想將 MockMvc 與 HtmlUnit 整合時,您有多種選擇:
-
MockMvc 和 HtmlUnit:如果您想使用原始的 HtmlUnit 庫,請選擇此選項。
-
MockMvc 和 WebDriver:使用此選項可以簡化開發並在整合測試和端到端測試之間重用程式碼。
-
MockMvc 和 Geb:如果您想使用 Groovy 進行測試、簡化開發並在整合測試和端到端測試之間重用程式碼,請選擇此選項。