Why HtmlUnit Integration?
需要记住的最明显的问题是“我为什么要使用此功能?
”答案可以通过探索一个非常基本的示例应用程序找到。假设你有一个 Spring MVC Web 应用程序,它支持对 Message
对象执行 CRUD 操作。该应用程序还支持对所有消息进行分页。你将如何对其进行测试?
The most obvious question that comes to mind is “Why do I need this?” The answer is
best found by exploring a very basic sample application. Assume you have a Spring MVC web
application that supports CRUD operations on a Message
object. The application also
supports paging through all messages. How would you go about testing it?
使用 Spring MVC Test,我们可以轻松测试是否可以创建 Message
,如下所示:
With Spring MVC Test, we can easily test if we are able to create a Message
, as follows:
-
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")
}
}
如果我们想要测试允许我们创建消息的表单视图,该怎么办?例如,假设我们的表单看起来如以下代码段所示:
What if we want to test the form view that lets us create the message? For example, assume our form looks like the following snippet:
<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>
我们如何确保我们的表单产生正确的请求以创建新消息?幼稚的尝试可能类似以下内容:
How do we ensure that our form produce the correct request to create a new message? A naive attempt might resemble the following:
-
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 表单与控制器不同步。为了解决此问题,我们可以将我们的两个测试合并在一起,如下所示:
This test has some obvious drawbacks. If we update our controller to use the parameter
message
instead of text
, our form test continues to pass, even though the HTML form
is out of sync with the controller. To resolve this we can combine our two tests, as
follows:
-
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")
}
这将降低我们的测试错误通过的风险,但仍有一些问题:
This would reduce the risk of our test incorrectly passing, but there are still some problems:
-
What if we have multiple forms on our page? Admittedly, we could update our XPath expressions, but they get more complicated as we take more factors into account: Are the fields the correct type? Are the fields enabled? And so on.
-
Another issue is that we are doing double the work we would expect. We must first verify the view, and then we submit the view with the same parameters we just verified. Ideally, this could be done all at once.
-
Finally, we still cannot account for some things. For example, what if the form has JavaScript validation that we wish to test as well?
总体问题在于,测试网页不涉及单一交互。相反,它是用户如何与网页交互以及该网页如何与其他资源交互的组合。例如,表单视图的结果用作用户创建消息的输入。此外,我们的表单视图有可能使用额外的资源来影响页面的行为,例如 JavaScript 验证。
The overall problem is that testing a web page does not involve a single interaction. Instead, it is a combination of how the user interacts with a web page and how that web page interacts with other resources. For example, the result of a form view is used as the input to a user for creating a message. In addition, our form view can potentially use additional resources that impact the behavior of the page, such as JavaScript validation.
Integration Testing to the Rescue?
为了解决前面提到的问题,我们可以执行端到端集成测试,但这种方法有一些缺点。考虑一下测试让我们对消息进行分页的视图。我们可能需要以下测试:
To resolve the issues mentioned earlier, we could perform end-to-end integration testing, but this has some drawbacks. Consider testing the view that lets us page through the messages. We might need the following tests:
-
Does our page display a notification to the user to indicate that no results are available when the messages are empty?
-
Does our page properly display a single message?
-
Does our page properly support paging?
要设置这些测试,我们需要确保我们的数据库中包含正确的消息。这会导致许多其他挑战:
To set up these tests, we need to ensure our database contains the proper messages. This leads to a number of additional challenges:
-
Ensuring the proper messages are in the database can be tedious. (Consider foreign key constraints.)
-
Testing can become slow, since each test would need to ensure that the database is in the correct state.
-
Since our database needs to be in a specific state, we cannot run tests in parallel.
-
Performing assertions on such items as auto-generated IDs, timestamps, and others can be difficult.
这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构我们的详细测试以使用运行速度更快、更可靠且没有副作用的模拟服务来减少端到端集成测试的数量。然后,我们可以实现少量真正的端到端集成测试,以验证简单的工作流程,以确保一切都正常运行。
These challenges do not mean that we should abandon end-to-end integration testing altogether. Instead, we can reduce the number of end-to-end integration tests by refactoring our detailed tests to use mock services that run much faster, more reliably, and without side effects. We can then implement a small number of true end-to-end integration tests that validate simple workflows to ensure that everything works together properly.
Enter HtmlUnit Integration
那么,如何在测试我们页面的交互与在我们的测试套件中仍然保持良好性能之间取得平衡呢?答案是:“将 MockMvc 与 HtmlUnit 集成。
”
So how can we achieve a balance between testing the interactions of our pages and still retain good performance within our test suite? The answer is: “By integrating MockMvc with HtmlUnit.”
HtmlUnit Integration Options
在你想要将 MockMvc 与 HtmlUnit 集成时,有许多选择:
You have a number of options when you want to integrate MockMvc with HtmlUnit:
-
MockMvc and HtmlUnit: Use this option if you want to use the raw HtmlUnit libraries.
-
MockMvc and WebDriver: Use this option to ease development and reuse code between integration and end-to-end testing.
-
MockMvc and Geb: Use this option if you want to use Groovy for testing, ease development, and reuse code between integration and end-to-end testing.