为什么选择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 进行测试、简化开发,并在集成测试和端到端测试之间重用代码,请选择此选项。