為什麼需要 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、時間戳記和其他項目執行斷言可能很困難。

這些挑戰並不意味著我們應該完全放棄端對端整合測試。 相反,我們可以透過重構我們的詳細測試以使用模擬服務來減少端對端整合測試的數量,這些模擬服務運行速度更快、更可靠且沒有副作用。 然後,我們可以實作少量真正的端對端整合測試,以驗證簡單的工作流程,以確保一切正常協同工作。

進入 HtmlUnit 整合

那麼,我們如何在測試頁面互動之間取得平衡,同時仍保持測試套件中的良好效能? 答案是:「透過將 MockMvc 與 HtmlUnit 整合。」

HtmlUnit 整合選項

當您想要將 MockMvc 與 HtmlUnit 整合時,您有多種選項

  • MockMvc 和 HtmlUnit:如果您想使用原始的 HtmlUnit 程式庫,請使用此選項。

  • MockMvc 和 WebDriver:使用此選項可簡化開發並在整合和端對端測試之間重複使用程式碼。

  • MockMvc 和 Geb:如果您想使用 Groovy 進行測試、簡化開發並在整合和端對端測試之間重複使用程式碼,請使用此選項。