为什么选择 HtmlUnit 集成?

想到的最明显的问题是“我为什么需要这个?”。通过探索一个非常基本的示例应用程序可以最好地找到答案。假设你有一个 Web MVC Web 应用程序,支持对 Message 对象进行 CRUD 操作。该应用程序还支持对所有消息进行分页。你会如何去测试它?

使用 Web MVC Test,我们可以轻松测试是否能够创建 Message,如下所示:

MockHttpServletRequestBuilder createMessage = post("/messages/")
    .param("summary", "Infra Rocks")
    .param("text", "In case you didn't know, Infra Rocks!");

mockMvc.perform(createMessage)
    .andExpect(status().is3xxRedirection())
    .andExpect(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>

我们如何确保我们的表单产生创建新消息的正确请求?一个幼稚的尝试可能类似于以下内容:

mockMvc.perform(get("/messages/form"))
		.andExpect(xpath("//input[@name='summary']").exists())
		.andExpect(xpath("//textarea[@name='text']").exists());

这个测试有一些明显的缺点。如果我们更新控制器以使用参数 message 而不是 text,我们的表单测试将继续通过,即使 HTML 表单与控制器不同步。为了解决这个问题,我们可以合并我们的两个测试,如下所示:

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, "Infra Rocks")
    .param(textParamName, "In case you didn't know, Infra Rocks!");

mockMvc.perform(createMessage)
    .andExpect(status().is3xxRedirection())
    .andExpect(redirectedUrl("/messages/123"));

这将降低我们测试错误通过的风险,但仍有一些问题:

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

  • 另一个问题是我们正在做我们预期的双倍工作。我们必须首先验证视图,然后我们使用我们刚刚验证的相同参数提交视图。理想情况下,这可以一次完成。

  • 最后,我们仍然无法解释某些事情。例如,如果表单具有我们也希望测试的 JavaScript 验证怎么办?

总体问题是测试网页不涉及单一交互。相反,它是用户如何与网页交互以及该网页如何与其他资源交互的组合。例如,表单视图的结果用作用户创建消息的输入。此外,我们的表单视图可能会使用影响页面行为的其他资源,例如 JavaScript 验证。

集成测试来救援?

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

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

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

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

为了设置这些测试,我们需要确保我们的数据库包含适当的消息。这导致了许多额外的挑战:

  • 确保数据库中包含适当的消息可能很乏味。(考虑外键约束。)

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

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

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

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

进入 HtmlUnit 集成

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

HtmlUnit 集成选项

当你想要集成 MockMvc 和 HtmlUnit 时,你有许多选项:

  • MockMvc 和 HtmlUnit:如果你想使用原始的 HtmlUnit 库,请使用此选项。

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

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