对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

为什么需要 HtmlUnit 集成?

人们脑海中首先浮现的最明显的问题是:“我为什么需要这个?”要找到最佳答案,最好通过一个非常基础的示例应用程序来探索。假设你有一个 Spring MVC Web 应用程序,它支持对 Message 对象执行 CRUD 操作,并且还支持对所有消息进行分页浏览。你会如何测试它呢?spring-doc.cadn.net.cn

使用 Spring MVC Test,我们可以轻松测试是否能够创建一个 Message,如下所示:spring-doc.cadn.net.cn

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")
	}
}

如果我们想要测试用于创建消息的表单视图该怎么办?例如, 假设我们的表单如下所示:spring-doc.cadn.net.cn

<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>

我们如何确保表单生成正确的请求以创建新消息呢?一个简单直接的尝试可能如下所示:spring-doc.cadn.net.cn

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 表单与控制器不同步,我们的表单测试仍然会通过。为了解决这个问题,我们可以将这两个测试合并,如下所示:spring-doc.cadn.net.cn

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")
}

这将降低我们的测试错误地通过的风险,但仍存在一些问题:spring-doc.cadn.net.cn

  • 如果我们页面上有多个表单怎么办?诚然,我们可以更新 XPath 表达式,但随着考虑的因素增多,这些表达式会变得更加复杂:字段类型是否正确?字段是否已启用?等等。spring-doc.cadn.net.cn

  • 另一个问题是,我们所做的工作量是我们预期的两倍。我们必须先验证视图,然后再使用刚刚验证过的相同参数提交该视图。理想情况下,这些操作可以一次性完成。spring-doc.cadn.net.cn

  • 最后,我们仍然无法处理某些情况。例如,如果表单包含我们希望一并测试的 JavaScript 验证,该怎么办?spring-doc.cadn.net.cn

总体问题在于,测试一个网页并不只涉及单一的交互。 相反,它结合了用户如何与网页交互,以及该网页如何与其他资源交互。 例如,表单视图的结果被用作用户创建消息时的输入。 此外,我们的表单视图还可能使用其他影响页面行为的资源,例如 JavaScript 验证。spring-doc.cadn.net.cn

集成测试来救场?

为了解决前面提到的问题,我们可以进行端到端的集成测试,但这存在一些缺点。考虑测试允许我们对消息进行分页浏览的视图,我们可能需要以下测试:spring-doc.cadn.net.cn

为了设置这些测试,我们需要确保数据库中包含正确的消息。这带来了一系列额外的挑战:spring-doc.cadn.net.cn

这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构详细的测试,使其使用模拟服务(mock services)来减少端到端集成测试的数量。这些模拟服务运行速度更快、更可靠,并且没有副作用。然后,我们可以实施少量真正的端到端集成测试,用于验证简单的业务流程,以确保所有组件能够正确协同工作。spring-doc.cadn.net.cn

进入 HtmlUnit 集成

那么,我们如何才能在测试页面交互的同时,仍保持测试套件的良好性能呢?答案是:“通过将 MockMvc 与 HtmlUnit 集成。”spring-doc.cadn.net.cn

HtmlUnit 集成选项

当你想要将 MockMvc 与 HtmlUnit 集成时,你有多种选择:spring-doc.cadn.net.cn