|
对于最新的稳定版本,请使用 Spring Framework 7.0.6! |
为何集成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 进行测试、简化开发并重用集成测试和端到端测试之间的代码,请选择此选项。