Why HtmlUnit Integration?

需要记住的最明显的问题是“我为什么要使用此功能?”答案可以通过探索一个非常基本的示例应用程序找到。假设你有一个 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 验证。

Integration Testing to the Rescue?

为了解决前面提到的问题,我们可以执行端到端集成测试,但这种方法有一些缺点。考虑一下测试让我们对消息进行分页的视图。我们可能需要以下测试:

  • 我们的页面是否向用户显示一条通知,指出当消息为空时没有可用的结果?

  • 我们的页面是否正确显示一条消息?

  • 我们的页面是否正确支持分页?

要设置这些测试,我们需要确保我们的数据库中包含正确的消息。这会导致许多其他挑战:

  • 确保数据库中存在正确的消息可能会很乏味。(考虑外键约束。)

  • 测试可能变得很慢,因为每个测试都需要确保数据库处于正确状态。

  • 由于我们的数据库需要处于特定状态,因此我们无法并行运行测试。

  • 对自动生成的 ID、时间戳等项目执行断言可能很困难。

这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构我们的详细测试以使用运行速度更快、更可靠且没有副作用的模拟服务来减少端到端集成测试的数量。然后,我们可以实现少量真正的端到端集成测试,以验证简单的工作流程,以确保一切都正常运行。

Enter HtmlUnit Integration

那么,如何在测试我们页面的交互与在我们的测试套件中仍然保持良好性能之间取得平衡呢?答案是:“将 MockMvc 与 HtmlUnit 集成。

HtmlUnit Integration Options

在你想要将 MockMvc 与 HtmlUnit 集成时,有许多选择:

  • MockMvc and HtmlUnit:如果您想使用原始 HtmlUnit 库,请使用此选项。

  • MockMvc and WebDriver:使用此选项可以简化集成和端到端测试之间的开发和代码重用。

  • MockMvc and Geb:如果您想使用 Groovy 进行测试,简化开发并在集成和端到端测试之间重用代码,请使用此选项。