The Spring MVC Test framework, also known as MockMvc, provides support for testing Spring MVC applications. It performs
full Spring MVC request handling but via mock request and response objects instead of a running server.
Spring MVC 测试框架,也称为 MockMvc,为测试 Spring MVC 应用程序提供支持。它通过模拟请求和响应对象来执行完整的 Spring MVC
请求处理,而不是通过运行中的服务器。
MockMvc can be used on its own to perform requests and verify responses. It can also be used through
the WebTestClient where MockMvc is plugged in as the server to handle requests with. The advantage of
WebTestClient is the option to work with higher level objects instead of raw data as well as the ability to switch to
full, end-to-end HTTP tests against a live server and use the same test API.
MockMvc 可以独立使用来执行请求并验证响应。它还可以通过 WebTestClient 使用,其中 MockMvc 作为服务器插入以处理请求。
WebTestClient 的优势是可以与更高级的对象一起工作,而不是原始数据,以及能够切换到针对实时服务器的完整、端到端 HTTP
测试并使用相同的测试 API。
You can write plain unit tests for Spring MVC by instantiating a controller, injecting it with dependencies, and calling
its methods.
您可以通过实例化一个控制器,注入依赖项并调用其方法来为 Spring MVC 编写平面单元测试。
However such tests do not verify request mappings, data binding, message conversion, type conversion, validation, and
nor do they involve any of the supporting @InitBinder, @ModelAttribute, or @ExceptionHandler methods.
然而,此类测试并不验证请求映射、数据绑定、消息转换、类型转换、验证,也不涉及任何支持方法 @InitBinder 、 @ModelAttribute 或
@ExceptionHandler 。
The Spring MVC Test framework, also known as MockMvc, aims to provide more complete testing for Spring MVC controllers
without a running server. It does that by invoking the DispatcherServlet and
passing “mock” implementations of the Servlet API from the spring-test module which
replicates the full Spring MVC request handling without a running server.
Spring MVC 测试框架,也称为 MockMvc ,旨在在不启动服务器的情况下为 Spring MVC 控制器提供更完整的测试。它是通过调用
DispatcherServlet 并传递来自 spring-test 模块的“模拟”Servlet API 实现来实现的,该模块在不启动服务器的情况下复制了完整的
Spring MVC 请求处理。
MockMvc is a server side test framework that lets you verify most of the functionality of a Spring MVC application using
lightweight and targeted tests. You can use it on its own to perform requests and to verify responses, or you can also
use it through the WebTestClient API with MockMvc plugged in as the server to handle requests with.
MockMvc 是一个服务器端测试框架,允许您通过轻量级和有针对性的测试验证 Spring MVC 应用程序的大部分功能。您可以使用它单独执行请求并验证响应,或者也可以通过
WebTestClient API 使用 MockMvc 作为服务器来处理请求。
When using MockMvc directly to perform requests, you’ll need static imports for:
当直接使用 MockMvc 进行请求时,您需要静态导入以下内容:
MockMvcBuilders.*
MockMvcRequestBuilders.*
MockMvcResultMatchers.*
MockMvcResultHandlers.*
An easy way to remember that is search for MockMvc*. If using Eclipse be sure to also add the above as “favorite
static members” in the Eclipse preferences.
一个记住它的简单方法是搜索 MockMvc* 。如果使用 Eclipse,请确保在 Eclipse 首选项中也将其添加为“收藏的静态成员”。
When using MockMvc through the WebTestClient you do not need static imports. The WebTestClient
provides a fluent API without static imports.
当通过 WebTestClient 使用 MockMvc 时,您不需要静态导入。 WebTestClient 提供了一个无需静态导入的流畅 API。
MockMvc can be setup in one of two ways. One is to point directly to the controllers you want to test and
programmatically configure Spring MVC infrastructure. The second is to point to Spring configuration with Spring MVC and
controller infrastructure in it.
MockMvc 可以通过两种方式之一进行设置。一种是指定要测试的控制器的直接路径,并程序化配置 Spring MVC 基础设施。第二种是指向包含
Spring MVC 和控制器基础设施的 Spring 配置。
To set up MockMvc for testing a specific controller, use the following:
为了设置 MockMvc 以测试特定控制器,请使用以下方法:
Or you can also use this setup when testing through the WebTestClient which
delegates to the same builder as shown above.
或者您也可以在通过 WebTestClient 进行测试时使用此设置,它将委托给上面显示的相同构建器。
To set up MockMvc through Spring configuration, use the following:
为了通过 Spring 配置设置 MockMvc,请使用以下方法:
Or you can also use this setup when testing through the WebTestClient which delegates to the same builder as shown above.
Which setup option should you use?
The webAppContextSetup loads your actual Spring MVC configuration, resulting in a more complete integration test.
Since the TestContext framework caches the loaded Spring configuration, it helps keep tests running fast, even as you
introduce more tests in your test suite.
webAppContextSetup 加载您的实际 Spring MVC 配置,从而实现更完整的集成测试。由于 TestContext 框架缓存了加载的 Spring
配置,即使在测试套件中引入更多测试时,它也有助于保持测试运行速度快。
Furthermore, you can inject mock services into controllers through Spring configuration to remain focused on testing the
web layer. The following example declares a mock service with Mockito:
此外,您可以通过 Spring 配置将模拟服务注入到控制器中,以保持专注于测试 Web 层。以下示例使用 Mockito 声明一个模拟服务:
You can then inject the mock service into the test to set up and verify your expectations, as the following example
shows:
您可以将模拟服务注入到测试中,以设置和验证您的期望,如下例所示:
The standaloneSetup, on the other hand, is a little closer to a unit test. It tests one controller at a time. You can
manually inject the controller with mock dependencies, and it does not involve loading Spring configuration.
standaloneSetup 另一方面,更接近单元测试。它一次测试一个控制器。您可以手动注入模拟依赖项到控制器中,并且不涉及加载 Spring
配置。
Such tests are more focused on style and make it easier to see which controller is being tested, whether any specific
Spring MVC configuration is required to work, and so on. The standaloneSetup is also a very convenient way to write
ad-hoc tests to verify specific behavior or to debug an issue.
此类测试更侧重于风格,使得更容易看到正在测试哪个控制器,是否需要特定的 Spring MVC 配置才能工作,等等。 standaloneSetup
也是一种非常方便的方式来编写即兴测试,以验证特定行为或调试问题。
As with most “integration versus unit testing” debates, there is no right or wrong answer. However, using the
standaloneSetup does imply the need for additional webAppContextSetup tests in order to verify your Spring MVC
configuration. Alternatively, you can write all your tests with webAppContextSetup, in order to always test against
your actual Spring MVC configuration.
与大多数“集成测试与单元测试”的争论一样,没有正确或错误的答案。然而,使用 standaloneSetup 确实意味着需要额外的
webAppContextSetup 测试来验证您的 Spring MVC 配置。或者,您可以使用 webAppContextSetup 编写所有测试,以确保始终针对实际的
Spring MVC 配置进行测试。
No matter which MockMvc builder you use, all MockMvcBuilder implementations provide some common and very useful
features. For example, you can declare an Accept header for all requests and expect a status of 200 as well as a
Content-Type header in all responses, as follows:
无论使用哪个 MockMvc 构建器,所有 MockMvcBuilder 实现都提供了一些常见且非常有用的功能。例如,您可以声明一个适用于所有请求的
Accept 头,并期望所有响应的状态为 200 以及一个 Content-Type 头,如下所示:
In addition, third-party frameworks (and applications) can pre-package setup instructions, such as those in a
MockMvcConfigurer. The Spring Framework has one such built-in implementation that helps to save and re-use the HTTP
session across requests. You can use it as follows:
此外,第三方框架(和应用程序)可以预先打包设置说明,例如在 MockMvcConfigurer 中。Spring 框架有一个这样的内置实现,可以帮助在请求之间保存和重用
HTTP 会话。您可以使用以下方式:
See the javadoc for
ConfigurableMockMvcBuilder
for a list of all MockMvc builder features or use the IDE to explore the available options.
查看 ConfigurableMockMvcBuilder 的 javadoc 以获取所有 MockMvc 构建器功能的列表,或使用 IDE 来探索可用选项。
This section shows how to use MockMvc on its own to perform requests and verify responses. If using MockMvc through the
WebTestClient please see the corresponding section on Writing Tests instead.
本节展示了如何单独使用 MockMvc 进行请求和验证响应。如果通过 WebTestClient 使用 MockMvc,请参阅编写测试的相关部分。
To perform requests that use any HTTP method, as the following example shows:
执行使用任何 HTTP 方法的请求,如下例所示:
You can also perform file upload requests that internally use MockMultipartHttpServletRequest so that there is no
actual parsing of a multipart request. Rather, you have to set it up to be similar to the following example:
您还可以执行使用 MockMultipartHttpServletRequest 内部处理的文件上传请求,这样实际上不需要解析多部分请求。相反,您需要将其设置成以下示例类似:
You can specify query parameters in URI template style, as the following example shows:
您可以在 URI 模板样式下指定查询参数,如下例所示:
You can also add Servlet request parameters that represent either query or form parameters, as the following example
shows:
您还可以添加表示查询参数或表单参数的 Servlet 请求参数,如下例所示:
If application code relies on Servlet request parameters and does not check the query string explicitly (as is most
often the case), it does not matter which option you use.
如果应用程序代码依赖于 Servlet 请求参数且没有显式检查查询字符串(如大多数情况),使用哪个选项都无关紧要。
Keep in mind, however, that query parameters provided with the URI template are decoded while request parameters
provided through the param(…) method are expected to already be decoded.
请注意,然而,与 URI 模板一起提供的查询参数在解码时,而通过 param(…) 方法提供的请求参数预期已经解码。
In most cases, it is preferable to leave the context path and the Servlet path out of the request URI. If you must test
with the full request URI, be sure to set the contextPath and servletPath accordingly so that request mappings work,
as the following example shows:
在大多数情况下,最好将上下文路径和 Servlet 路径从请求 URI 中排除。如果您必须使用完整的请求 URI 进行测试,请确保相应地设置
contextPath 和 servletPath ,以便请求映射能够正常工作,如下例所示:
In the preceding example, it would be cumbersome to set the contextPath and servletPath with every performed
request. Instead, you can set up default request properties, as the following example shows:
The preceding properties affect every request performed through the MockMvc instance. If the same property is also
specified on a given request, it overrides the default value. That is why the HTTP method and URI in the default request
do not matter, since they must be specified on every request.
You can define expectations by appending one or more andExpect(..) calls after performing a request, as the following
example shows. As soon as one expectation fails, no other expectations will be asserted.
You can define multiple expectations by appending andExpectAll(..) after performing a request, as the following
example shows. In contrast to andExpect(..), andExpectAll(..) guarantees that all supplied expectations will be
asserted and that all failures will be tracked and reported.
您可以在执行请求后通过追加 andExpectAll(..) 来定义多个期望,如下例所示。与 andExpect(..) 相比, andExpectAll(..)
保证所有提供的期望都将被断言,并且所有失败都将被跟踪和报告。
MockMvcResultMatchers.* provides a number of expectations, some of which are further nested with more detailed
expectations.
MockMvcResultMatchers.* 提供了多个期望,其中一些期望被进一步嵌套,包含更详细的期望。
Expectations fall in two general categories. The first category of assertions verifies properties of the response (for
example, the response status, headers, and content). These are the most important results to assert.
预期分为两大类。第一类断言验证响应的性质(例如,响应状态、头信息和内容)。这些是最重要的断言结果。
The second category of assertions goes beyond the response.
第二类断言超越了响应。
These assertions let you inspect Spring MVC specific aspects, such as which controller method processed the request,
whether an exception was raised and handled, what the content of the model is, what view was selected, what flash
attributes were added, and so on.
这些断言让您可以检查 Spring MVC
的特定方面,例如哪个控制器方法处理了请求,是否抛出了异常并被处理,模型的内容是什么,选择了哪个视图,添加了哪些闪存属性等等。
They also let you inspect Servlet specific aspects, such as request and session attributes.
它们还允许您检查 Servlet 特定的方面,例如请求和会话属性。
The following test asserts that binding or validation failed:
以下测试断言绑定或验证失败:
Many times, when writing tests, it is useful to dump the results of the performed request. You can do so as follows,
where print() is a static import from MockMvcResultHandlers:
很多时候,在编写测试时,输出执行请求的结果很有用。您可以按照以下方式操作,其中 print() 是从 MockMvcResultHandlers 的静态导入:
As long as request processing does not cause an unhandled exception, the print() method prints all the available
result data to System.out. There is also a log() method and two additional variants of the print() method, one
that accepts an OutputStream and one that accepts a Writer. For example, invoking print(System.err) prints the
result data to System.err, while invoking print(myWriter) prints the result data to a custom writer. If you want to
have the result data logged instead of printed, you can invoke the log() method, which logs the result data as a
single DEBUG message under the org.springframework.test.web.servlet.result logging category.
只要请求处理不会引发未处理的异常, print() 方法将所有可用的结果数据打印到 System.out 。还有一个 log() 方法以及
print() 方法的两个附加变体,一个接受一个 OutputStream ,另一个接受一个 Writer 。例如,调用 print(System.err)
将结果数据打印到 System.err ,而调用 print(myWriter) 将结果数据打印到自定义的写入器。如果您希望将结果数据记录下来而不是打印出来,可以调用
log() 方法,该方法将结果数据作为单个 DEBUG 消息记录在 org.springframework.test.web.servlet.result 记录类别下。
In some cases, you may want to get direct access to the result and verify something that cannot be verified otherwise.
This can be achieved by appending .andReturn() after all other expectations, as the following example shows:
在某些情况下,您可能希望直接访问结果并验证其他方式无法验证的内容。这可以通过在所有其他期望之后附加 .andReturn()
来实现,如下例所示:
If all tests repeat the same expectations, you can set up common expectations once when building the MockMvc instance,
as the following example shows:
如果所有测试都重复相同的期望,您可以在构建 MockMvc 实例时一次性设置共同期望,如下例所示:
Note that common expectations are always applied and cannot be overridden without creating a separate MockMvc
instance.
请注意,常见的期望总是被应用,并且不能在不创建单独的 MockMvc 实例的情况下被覆盖。
When a JSON response content contains hypermedia links created
with Spring HATEOAS, you can verify the resulting links by using
JsonPath expressions, as the following example shows:
当 JSON 响应内容包含使用 Spring HATEOAS 创建的超媒体链接时,您可以使用 JsonPath 表达式验证生成的链接,如下例所示:
When XML response content contains hypermedia links created
with Spring HATEOAS, you can verify the resulting links by using
XPath expressions:
当 XML 响应内容包含使用 Spring HATEOAS 创建的超媒体链接时,您可以使用 XPath 表达式验证生成的链接:
This section shows how to use MockMvc on its own to test asynchronous request handling. If using MockMvc through
the WebTestClient, there is nothing special to do to make asynchronous requests work as the
WebTestClient automatically does what is described in this section.
本节展示了如何仅使用 MockMvc 来测试异步请求处理。如果通过 WebTestClient 使用 MockMvc,要使异步请求正常工作,无需进行任何特殊操作,因为
WebTestClient 会自动执行本节中描述的操作。
Servlet 3.0 asynchronous
requests, supported in Spring MVC,
work by exiting the Servlet container thread and allowing the application to compute the response asynchronously, after
which an async dispatch is made to complete processing on a Servlet container thread.
Servlet 3.0 异步请求,在 Spring MVC 中受支持,通过退出 Servlet 容器线程,允许应用程序异步计算响应,之后在 Servlet
容器线程上完成处理的异步调度。
In Spring MVC Test, async requests can be tested by asserting the produced async value first, then manually performing
the async dispatch, and finally verifying the response. Below is an example test for controller methods that return
DeferredResult, Callable, or reactive type such as Reactor Mono:
在 Spring MVC Test 中,可以通过首先断言生成的异步值,然后手动执行异步分发,最后验证响应来测试异步请求。以下是一个针对返回
DeferredResult 、 Callable 或类似 Reactor Mono 这样的响应式类型的控制器方法的示例测试:
1
Check response status is still unchanged
检查响应状态是否仍然未变
2
Async processing must have started
异步处理必须已开始
3
Wait and assert the async result
等待并断言异步结果
4
Manually perform an ASYNC dispatch (as there is no running container)
手动执行异步调度(因为没有正在运行的容器)
5
Verify the final response
验证最终响应
1
Check response status is still unchanged
2
Async processing must have started
3
Wait and assert the async result
4
Manually perform an ASYNC dispatch (as there is no running container)
5
Verify the final response
You can use WebTestClient to test streaming responses such as Server-Sent Events. However,
MockMvcWebTestClient doesn’t support infinite streams because there is no way to cancel the server stream from the
client side. To test infinite streams, you’ll need to bind to a running server, or when
using Spring
Boot, test with a running server.
您可以使用 WebTestClient 来测试流式响应,例如服务器端事件。然而, MockMvcWebTestClient
不支持无限流,因为客户端无法取消服务器流。要测试无限流,您需要绑定到一个正在运行的服务器,或者在 Spring Boot
中使用时,与一个正在运行的服务器进行测试。
When setting up a MockMvc instance, you can register one or more Servlet Filter instances, as the following example
shows:
当设置一个 MockMvc 实例时,您可以注册一个或多个 Servlet Filter 实例,如下例所示:
Registered filters are invoked through the MockFilterChain from spring-test, and the last filter delegates to the
DispatcherServlet.
已注册的过滤器通过 MockFilterChain 从 spring-test 调用,最后一个过滤器委托给 DispatcherServlet 。
MockMvc 与端到端测试
MockMVc is built on Servlet API mock implementations from the spring-test module and does not rely on a running
container. Therefore, there are some differences when compared to full end-to-end integration tests with an actual
client and a live server running.
MockMVc 基于 spring-test 模块的 Servlet API 模拟实现构建,不依赖于运行中的容器。因此,与使用实际客户端和运行中的服务器进行的完整端到端集成测试相比,存在一些差异。
The easiest way to think about this is by starting with a blank MockHttpServletRequest. Whatever you add to it is what
the request becomes. Things that may catch you by surprise are that there is no context path by default; no jsessionid
cookie; no forwarding, error, or async dispatches; and, therefore, no actual JSP rendering. Instead, “forwarded” and
“redirected” URLs are saved in the MockHttpServletResponse and can be asserted with expectations.
最容易思考这个问题的方式是从一个空的 MockHttpServletRequest 开始。你添加到其中的任何内容就是请求变成的内容。可能会让你感到意外的是,默认情况下没有上下文路径;没有
jsessionid cookie;没有转发、错误或异步调度;因此,没有实际的 JSP 渲染。相反,“转发”和“重定向”的 URL 被保存在
MockHttpServletResponse 中,并且可以用期望来断言。
This means that, if you use JSPs, you can verify the JSP page to which the request was forwarded, but no HTML is
rendered. In other words, the JSP is not invoked.
这意味着,如果您使用 JSP,您可以验证请求被转发到的 JSP 页面,但没有 HTML 被渲染。换句话说,JSP 没有被调用。
Note, however, that all other rendering technologies that do not rely on forwarding, such as Thymeleaf and Freemarker,
render HTML to the response body as expected. The same is true for rendering JSON, XML, and other formats through
@ResponseBody methods.
请注意,然而,所有不依赖于转发的其他渲染技术,如 Thymeleaf 和 Freemarker,都能按预期将 HTML 渲染到响应体中。同样,通过
@ResponseBody 方法渲染 JSON、XML 和其他格式也是如此。
Alternatively, you may consider the full end-to-end integration testing support from Spring Boot with @SpringBootTest.
See
the Spring Boot Reference Guide.
或者,您可以考虑使用 Spring Boot 的 @SpringBootTest 提供的完整端到端集成测试支持。请参阅 Spring Boot 参考指南。
There are pros and cons for each approach. The options provided in Spring MVC Test are different stops on the scale from
classic unit testing to full integration testing.
每种方法都有利弊。Spring MVC Test 提供的选项是在经典单元测试到全面集成测试的尺度上的不同停点。
To be certain, none of the options in Spring MVC Test fall under the category of classic unit testing, but they are a
little closer to it.
当然,Spring MVC Test 中的任何选项都不属于经典单元测试的范畴,但它们与之略为接近。
For example, you can isolate the web layer by injecting mocked services into controllers, in which case you are testing
the web layer only through the DispatcherServlet but with actual Spring configuration, as you might test the data
access layer in isolation from the layers above it. Also, you can use the stand-alone setup, focusing on one controller
at a time and manually providing the configuration required to make it work.
例如,您可以通过向控制器中注入模拟服务来隔离网络层,在这种情况下,您仅通过 DispatcherServlet 测试网络层,但使用实际的
Spring 配置,就像您可能从上面的层中独立测试数据访问层一样。此外,您还可以使用独立设置,一次关注一个控制器,并手动提供使其工作的配置。
Another important distinction when using Spring MVC Test is that, conceptually, such tests are the server-side, so you
can check what handler was used, if an exception was handled with a HandlerExceptionResolver, what the content of the
model is, what binding errors there were, and other details.
使用 Spring MVC Test 时,另一个重要的区别是,从概念上讲,这些测试是服务器端的,因此您可以检查使用了哪个处理器,是否通过
HandlerExceptionResolver 处理了异常,模型的内容是什么,有哪些绑定错误,以及其他细节。
That means that it is easier to write expectations, since the server is not an opaque box, as it is when testing it
through an actual HTTP client.
这意味着编写预期更容易,因为服务器不是一个不透明的盒子,就像通过实际的 HTTP 客户端进行测试时那样。
This is generally an advantage of classic unit testing: It is easier to write, reason about, and debug but does not
replace the need for full integration tests.
这是经典单元测试的一般优势:它更容易编写、推理和调试,但并不取代全面集成测试的需求。
At the same time, it is important not to lose sight of the fact that the response is the most important thing to check.
In short, there is room here for multiple styles and strategies of testing even within the same project.
同时,重要的是不要忽视响应是最需要检查的事情。简而言之,即使在同一项目中,这里也有空间采用多种测试风格和策略。
The framework’s own tests
include many sample tests
intended to show how to use MockMvc on its own or through
the WebTestClient.
Browse these examples for further ideas.
该框架的自身测试包括许多示例测试,旨在展示如何单独或通过 WebTestClient 使用 MockMvc。浏览这些示例以获取更多灵感。
3.7.2. HtmlUnit 集成
Spring provides integration between MockMvc and HtmlUnit.
This simplifies performing end-to-end testing when using HTML-based views. This integration lets you:
Spring 为 MockMvc 和 HtmlUnit 提供了集成。这简化了在使用基于 HTML 的视图时进行端到端测试的操作。此集成允许您:
Easily test HTML pages by using tools such
as HtmlUnit, WebDriver,
and Geb without the need to deploy to a Servlet
container.
轻松使用 HtmlUnit、WebDriver 和 Geb 等工具测试 HTML 页面,无需部署到 Servlet 容器。
Test JavaScript within pages.
在页面中测试 JavaScript。
Optionally, test using mock services to speed up testing.
可选地,使用模拟服务来加速测试。
Share logic between in-container end-to-end tests and out-of-container integration tests.
在容器内端到端测试和容器外集成测试之间共享逻辑。
MockMvc works with templating technologies that do not rely on a Servlet Container (for example, Thymeleaf, FreeMarker,
and others), but it does not work with JSPs, since they rely on the Servlet container.
MockMvc 与不依赖于 Servlet 容器的模板技术(例如,Thymeleaf、FreeMarker 等)一起工作,但它不与 JSPs 一起工作,因为它们依赖于
Servlet 容器。
为什么选择 HtmlUnit 集成?
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?
最明显的问题就是“我为什么需要这个?”最好的答案是通过探索一个非常基础的示例应用程序来找到。假设你有一个支持在 Message
对象上执行 CRUD 操作的 Spring MVC Web 应用程序。该应用程序还支持对所有消息进行分页。你会如何测试它呢?
With Spring MVC Test, we can easily test if we are able to create a Message, as follows:
使用 Spring MVC Test,我们可以轻松地测试我们是否能够创建一个 Message ,如下所示:
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:
如果我们想测试创建消息的表单视图呢?例如,假设我们的表单看起来像以下片段:
How do we ensure that our form produce the correct request to create a new message? A naive attempt might resemble the
following:
我们如何确保我们的表单能够正确地创建新消息的请求?一个简单的尝试可能类似于以下内容:
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 synch with the controller. To resolve this we can
combine our two tests, as follows:
这个测试有一些明显的缺点。如果我们更新我们的控制器以使用参数 message 而不是 text ,我们的表单测试仍然会通过,尽管 HTML
表单与控制器不同步。为了解决这个问题,我们可以将我们的两个测试合并,如下所示:
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.
如果我们页面上有多个表单怎么办?诚然,我们可以更新我们的 XPath 表达式,但随着我们考虑更多因素,它们会变得更加复杂:字段类型是否正确?字段是否启用?等等。
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.
例如,表单视图的结果被用作用户创建消息的输入。此外,我们的表单视图可能还会使用影响页面行为的额外资源,例如 JavaScript 验证。
集成测试拯救了吗?
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.
对自动生成的 ID、时间戳等此类项目进行断言可能很困难。
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.
我们可以实施少量真正的端到端集成测试,以验证简单的流程,确保一切协同工作正常。
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.”
You have a number of options when you want to integrate MockMvc with HtmlUnit:
您在想要集成 MockMvc 与 HtmlUnit 时有许多选择:
MockMvc and HtmlUnit: Use this option if you want to use the raw HtmlUnit
libraries.
MockMvc 和 HtmlUnit:如果您想使用原始的 HtmlUnit 库,请使用此选项。
MockMvc and WebDriver: Use this option to ease development and reuse
code between integration and end-to-end testing.
MockMvc 和 WebDriver:使用此选项可简化开发并促进集成测试和端到端测试之间的代码重用。
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.
MockMvc 和 Geb:如果您想使用 Groovy 进行测试、简化开发以及在不同测试阶段(集成测试和端到端测试)之间重用代码,请使用此选项。
This section describes how to integrate MockMvc and HtmlUnit. Use this option if you want to use the raw HtmlUnit
libraries.
本节描述了如何集成 MockMvc 和 HtmlUnit。如果您想使用原始的 HtmlUnit 库,请使用此选项。
MockMvc 和 HtmlUnit 设置
First, make sure that you have included a test dependency on net.sourceforge.htmlunit:htmlunit. In order to use
HtmlUnit with Apache HttpComponents 4.5+, you need to use HtmlUnit 2.18 or higher.
首先,请确保您已包含对 net.sourceforge.htmlunit:htmlunit 的测试依赖项。为了使用 HtmlUnit 与 Apache HttpComponents
4.5+,您需要使用 HtmlUnit 2.18 或更高版本。
We can easily create an HtmlUnit WebClient that integrates with MockMvc by using the MockMvcWebClientBuilder, as
follows:
我们可以轻松地创建一个与 MockMvc 集成的 HtmlUnit WebClient ,如下所示:
This is a simple example of using MockMvcWebClientBuilder. For advanced usage, see Advanced
MockMvcWebClientBuilder.
这是一个使用 MockMvcWebClientBuilder 的简单示例。对于高级用法,请参阅高级 MockMvcWebClientBuilder 。
This ensures that any URL that references localhost as the server is directed to our MockMvc instance without the
need for a real HTTP connection. Any other URL is requested by using a network connection, as normal. This lets us
easily test the use of CDNs.
这确保了任何将 localhost 作为服务器的 URL 都会被导向我们的 MockMvc 实例,无需建立真实的 HTTP 连接。其他任何 URL
都会通过正常的网络连接请求。这使得我们能够轻松测试 CDN 的使用。
MockMvc 和 HtmlUnit 使用
Now we can use HtmlUnit as we normally would but without the need to deploy our application to a Servlet container. For
example, we can request the view to create a message with the following:
现在我们可以像平时一样使用 HtmlUnit,但无需将我们的应用程序部署到 Servlet 容器中。例如,我们可以使用以下方式请求视图创建消息:
The default context path is "". Alternatively, we can specify the context path, as described in Advanced
MockMvcWebClientBuilder.
默认上下文路径为 "" 。或者,我们可以指定上下文路径,如高级 MockMvcWebClientBuilder 所述。
Once we have a reference to the HtmlPage, we can then fill out the form and submit it to create a message, as the
following example shows:
一旦我们有了对 HtmlPage 的引用,我们就可以填写表格并提交以创建消息,如下例所示:
Finally, we can verify that a new message was created successfully. The following assertions use
the AssertJ library:
最后,我们可以验证一条新消息已成功创建。以下断言使用了 AssertJ 库:
The preceding code improves on our MockMvc test in a number of ways.
First, we no longer have to explicitly verify our form and then create a request that looks like the form. Instead, we
request the form, fill it out, and submit it, thereby significantly reducing the overhead.
前述代码在多个方面改进了我们的 MockMvc 测试。首先,我们不再需要显式验证我们的表单然后创建一个看起来像表单的请求。相反,我们请求表单,填写它,然后提交,从而显著减少了开销。
Another important factor is
that HtmlUnit uses the Mozilla Rhino engine to evaluate JavaScript.
This means that we can also test the behavior of JavaScript within our pages.
另一个重要因素是 HtmlUnit 使用 Mozilla Rhino 引擎来评估 JavaScript。这意味着我们也可以测试我们页面中 JavaScript 的行为。
See the HtmlUnit documentation for additional information about
using HtmlUnit.
查看 HtmlUnit 文档以获取有关使用 HtmlUnit 的更多信息。
MockMvcWebClientBuilder 高级MockMvcWebClientBuilder
In the examples so far, we have used MockMvcWebClientBuilder in the simplest way possible, by building a WebClient
based on the WebApplicationContext loaded for us by the Spring TestContext Framework. This approach is repeated in the
following example:
在迄今为止的示例中,我们以最简单的方式使用了 MockMvcWebClientBuilder ,通过基于 Spring TestContext 框架为我们加载的
WebApplicationContext 构建了一个 WebClient 。这种方法在以下示例中重复使用:
We can also specify additional configuration options, as the following example shows:
我们还可以指定额外的配置选项,如下例所示:
As an alternative, we can perform the exact same setup by configuring the MockMvc instance separately and supplying it
to the MockMvcWebClientBuilder, as follows:
作为替代方案,我们可以通过单独配置 MockMvc 实例并将其提供给 MockMvcWebClientBuilder ,以执行完全相同的设置:
This is more verbose, but, by building the WebClient with a MockMvc instance, we have the full power of MockMvc at
our fingertips.
这是更冗长的描述,但是,通过使用 MockMvc 实例构建 WebClient ,我们就能完全掌握 MockMvc 的全部功能。
For additional information on creating a MockMvc instance,
see Setup Choices.
关于创建 MockMvc 实例的更多信息,请参阅设置选项。
In the previous sections, we have seen how to use MockMvc in conjunction with the raw HtmlUnit APIs. In this section, we
use additional abstractions within the Selenium WebDriver to make
things even easier.
在前几节中,我们看到了如何结合使用 MockMvc 和原始 HtmlUnit API。在本节中,我们使用 Selenium WebDriver 中的额外抽象来使事情变得更加简单。
为什么选择 WebDriver 和 MockMvc?
We can already use HtmlUnit and MockMvc, so why would we want to use WebDriver? The Selenium WebDriver provides a very
elegant API that lets us easily organize our code. To better show how it works, we explore an example in this section.
我们已经有 HtmlUnit 和 MockMvc 了,为什么还要使用 WebDriver 呢?Selenium WebDriver 提供了一个非常优雅的
API,让我们可以轻松组织代码。为了更好地展示其工作原理,本节我们将探讨一个示例。
Despite being a part of Selenium, WebDriver does not require a Selenium Server to run
your tests.
尽管是 Selenium 的一部分,WebDriver 运行测试时不需要 Selenium 服务器。
Suppose we need to ensure that a message is created properly. The tests involve finding the HTML form input elements, filling them out, and making various assertions.
This approach results in numerous separate tests because we want to test error conditions as well. For example, we want to ensure that we get an error if we fill out only part of the form. If we fill out the entire form, the newly created message should be displayed afterwards.
If one of the fields were named “summary”, we might have something that resembles the following repeated in multiple
places within our tests:
如果其中一个字段被命名为“摘要”,我们可能会在我们的测试中多次看到以下内容重复出现:
So what happens if we change the id to smmry? Doing so would force us to update all of our tests to incorporate this
change. This violates the DRY principle, so we should ideally extract this code into its own method, as follows:
那么,如果我们把 id 改为 smmry 会发生什么?这样做将迫使我们更新所有测试以包含这个更改。这违反了 DRY
原则,因此我们理想情况下应该将此代码提取到自己的方法中,如下所示:
Doing so ensures that we do not have to update all of our tests if we change the UI.
这样做确保了如果我们更改 UI,我们不需要更新所有的测试。
We might even take this a step further and place this logic within an Object that represents the HtmlPage we are
currently on, as the following example shows:
我们甚至可以更进一步,将这个逻辑放置在一个代表我们当前所在的 HtmlPage 的 Object 中,如下例所示:
Formerly, this pattern was known as the Page Object Pattern.
While we can certainly do this with HtmlUnit, WebDriver provides some tools that we explore in the following sections to
make this pattern much easier to implement.
以前,这种模式被称为页面对象模式。虽然我们当然可以用 HtmlUnit 做到这一点,但 WebDriver
提供了一些工具,我们将在接下来的章节中探讨这些工具,使这种模式更容易实现。
MockMvc 和 WebDriver 配置
To use Selenium WebDriver with the Spring MVC Test framework, make sure that your project includes a test dependency on
org.seleniumhq.selenium:selenium-htmlunit-driver.
要使用 Selenium WebDriver 与 Spring MVC Test 框架,请确保您的项目包含对
org.seleniumhq.selenium:selenium-htmlunit-driver 的测试依赖项。
We can easily create a Selenium WebDriver that integrates with MockMvc by using the MockMvcHtmlUnitDriverBuilder as
the following example shows:
我们可以轻松创建一个与 MockMvc 集成的 Selenium WebDriver,如下例所示:
This is a simple example of using MockMvcHtmlUnitDriverBuilder. For more advanced usage, see Advanced
MockMvcHtmlUnitDriverBuilder.
这是一个使用 MockMvcHtmlUnitDriverBuilder 的简单示例。对于更高级的使用,请参阅高级 MockMvcHtmlUnitDriverBuilder 。
The preceding example ensures that any URL that references localhost as the server is directed to our MockMvc
instance without the need for a real HTTP connection. Any other URL is requested by using a network connection, as
normal. This lets us easily test the use of CDNs.
前一个示例确保任何将 localhost 作为服务器的 URL 都会被导向我们的 MockMvc 实例,无需建立真实的 HTTP 连接。任何其他 URL
都会通过正常的网络连接请求,这样我们可以轻松测试 CDN 的使用。
MockMvc 和 WebDriver 使用
Now we can use WebDriver as we normally would but without the need to deploy our application to a Servlet container. For
example, we can request the view to create a message with the following:
现在我们可以像平时一样使用 WebDriver,但无需将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求视图创建以下消息:
1
CreateMessagePage extends the AbstractPage. We do not go over the details of AbstractPage, but, in summary, it
contains common functionality for all of our pages. For example, if our application has a navigational bar, global error
messages, and other features, we can place this logic in a shared location.
CreateMessagePage 扩展了 AbstractPage 。我们不过多讨论细节。 AbstractPage ,但总的来说,它包含了我们所有页面共有的功能。
例如,如果我们的应用程序有一个导航栏、全局错误消息和其他 特性,我们可以将此逻辑放置在共享位置。
2
We have a member variable for each of the parts of the HTML page in which we are interested. These are of type
WebElement. WebDriver’s PageFactory lets us remove a lot
of code from the HtmlUnit version of CreateMessagePage by automatically resolving each WebElement. The
PageFactory#initElements(WebDriver,Class<T>)
method automatically resolves each WebElement by using the field name and looking it up by the id or name of the
element within the HTML page.
我们为 HTML 页面中的每个部分都有一个成员变量 感兴趣。这些属于类型 WebElement 。WebDriver 的 PageFactory 让我们删除
大量来自 HtmlUnit 版本 CreateMessagePage 的代码,通过自动解析 每个 WebElement 。
PageFactory#initElements(WebDriver,Class<T>) 方法自动通过字段名称查找来解决每个 WebElement 通过 HTML 页面中元素的
id 或 name 。
3
We can use the
@FindBy annotation
to override the default lookup behavior. Our example shows how to use the @FindBy annotation to look up our submit
button with a css selector (input[type=submit]).
我们可以使用 @FindBy 注释 覆盖默认查找行为。我们的示例展示了如何使用 @FindBy 注释以查找我们的提交按钮,使用 css
选择器(input[type=submit])。
We can then fill out the form and submit it to create a message, as follows:
我们可以填写表格并提交以创建消息,如下所示:
1
CreateMessagePage extends the AbstractPage. We do not go over the details of AbstractPage, but, in summary, it
contains common functionality for all of our pages. For example, if our application has a navigational bar, global error
messages, and other features, we can place this logic in a shared location.
CreateMessagePage 扩展了 AbstractPage 。我们不过多讨论细节。 AbstractPage ,但总的来说,它包含了我们所有页面共有的功能。
例如,如果我们的应用程序有一个导航栏、全局错误消息和其他 特性,我们可以将此逻辑放置在共享位置。
2
We have a member variable for each of the parts of the HTML page in which we are interested. These are of type
WebElement. WebDriver’s PageFactory lets us remove a lot
of code from the HtmlUnit version of CreateMessagePage by automatically resolving each WebElement. The
PageFactory#initElements(WebDriver,Class<T>)
method automatically resolves each WebElement by using the field name and looking it up by the id or name of the
element within the HTML page.
我们为 HTML 页面中的每个部分都有一个成员变量 感兴趣。这些属于类型 WebElement 。WebDriver 的 PageFactory 让我们删除
大量来自 HtmlUnit 版本 CreateMessagePage 的代码,通过自动解析 每个 WebElement 。
PageFactory#initElements(WebDriver,Class<T>) 方法自动通过字段名称查找来解决每个 WebElement 通过 HTML 页面中元素的
id 或 name 。
3
We can use the
@FindBy annotation
to override the default lookup behavior. Our example shows how to use the @FindBy annotation to look up our submit
button with a css selector (input[type=submit]).
我们可以使用 @FindBy 注释 覆盖默认查找行为。我们的示例展示了如何使用 @FindBy 注释以查找我们的提交按钮,使用 css
选择器(input[type=submit])。
This improves on the design of our HtmlUnit test by leveraging the Page
Object Pattern. As we mentioned in Why WebDriver and MockMvc?, we can
use the Page Object Pattern with HtmlUnit, but it is much easier with WebDriver. Consider the following
CreateMessagePage implementation:
这通过利用页面对象模式改进了我们的 HtmlUnit 测试设计。正如我们在《为什么选择 WebDriver 和 MockMvc?》中提到的,我们可以使用页面对象模式与
HtmlUnit 一起使用,但与 WebDriver 一起使用则更容易。考虑以下 CreateMessagePage 实现:
Finally, we can verify that a new message was created successfully. The following assertions use
the AssertJ assertion library:
最后,我们可以验证一条新消息已成功创建。以下断言使用了 AssertJ 断言库:
We can see that our ViewMessagePage lets us interact with our custom domain model. For example, it exposes a method
that returns a Message object:
我们可以看到我们的 ViewMessagePage 允许我们与我们的自定义域模型交互。例如,它暴露了一个返回 Message 对象的方法:
We can then use the rich domain objects in our assertions.
我们可以然后在我们的断言中使用丰富的领域对象。
Lastly, we must not forget to close the WebDriver instance when the test is complete, as follows:
最后,测试完成后,我们务必不要忘记关闭 WebDriver 实例,如下所示:
For additional information on using WebDriver, see the
Selenium WebDriver documentation.
关于使用 WebDriver 的更多信息,请参阅 Selenium WebDriver 文档。
MockMvcHtmlUnitDriverBuilder 高级MockMvcHtmlUnitDriverBuilder
In the examples so far, we have used MockMvcHtmlUnitDriverBuilder in the simplest way possible, by building a
WebDriver based on the WebApplicationContext loaded for us by the Spring TestContext Framework. This approach is
repeated here, as follows:
在迄今为止的示例中,我们以最简单的方式使用了 MockMvcHtmlUnitDriverBuilder ,通过基于 Spring TestContext 框架为我们加载的
WebApplicationContext 构建了一个 WebDriver 。这种方法在此处重复使用,如下所示:
We can also specify additional configuration options, as follows:
我们还可以指定其他配置选项,如下所示:
As an alternative, we can perform the exact same setup by configuring the MockMvc instance separately and supplying it
to the MockMvcHtmlUnitDriverBuilder, as follows:
作为替代方案,我们可以通过单独配置 MockMvc 实例并将其提供给 MockMvcHtmlUnitDriverBuilder ,以执行完全相同的设置:
This is more verbose, but, by building the WebDriver with a MockMvc instance, we have the full power of MockMvc at
our fingertips.
这是更冗长的描述,但是,通过使用 MockMvc 实例构建 WebDriver ,我们就能完全掌握 MockMvc 的全部功能。
For additional information on creating a MockMvc instance,
see Setup Choices.
关于创建 MockMvc 实例的更多信息,请参阅设置选项。
In the previous section, we saw how to use MockMvc with WebDriver. In this section, we
use Geb to make our tests even Groovy-er.
在上一节中,我们看到了如何使用 MockMvc 和 WebDriver。在本节中,我们使用 Geb 使我们的测试更加 Groovy。
为什么选择 Geb 和 MockMvc?
Geb is backed by WebDriver, so it offers many of the same benefits
that we get from WebDriver. However, Geb makes things even easier by taking care of some of the boilerplate code for
us.
Geb 由 WebDriver 支持,因此它提供了我们从 WebDriver 中获得的大部分相同好处。然而,Geb 通过为我们处理一些样板代码,使事情变得更加简单。
MockMvc 和 Geb 设置
We can easily initialize a Geb Browser with a Selenium WebDriver that uses MockMvc, as follows:
我们可以轻松地使用 Selenium WebDriver 和 MockMvc 初始化一个 Geb Browser
This is a simple example of using MockMvcHtmlUnitDriverBuilder. For more advanced usage, see Advanced
MockMvcHtmlUnitDriverBuilder.
这是一个使用 MockMvcHtmlUnitDriverBuilder 的简单示例。对于更高级的使用,请参阅高级 MockMvcHtmlUnitDriverBuilder 。
This ensures that any URL referencing localhost as the server is directed to our MockMvc instance without the need
for a real HTTP connection. Any other URL is requested by using a network connection as normal. This lets us easily test
the use of CDNs.
MockMvc 和 Geb 使用
Now we can use Geb as we normally would but without the need to deploy our application to a Servlet container. For
example, we can request the view to create a message with the following:
现在我们可以像平时一样使用 Geb,但无需将我们的应用程序部署到 Servlet 容器中。例如,我们可以通过以下方式请求视图创建消息:
We can then fill out the form and submit it to create a message, as follows:
我们可以填写表格并提交以创建消息,如下所示:
Any unrecognized method calls or property accesses or references that are not found are forwarded to the current page
object. This removes a lot of the boilerplate code we needed when using WebDriver directly.
任何未识别的方法调用、属性访问或未找到的引用都将转发到当前页面对象。这消除了我们直接使用 WebDriver 时所需的大量样板代码。
As with direct WebDriver usage, this improves on the design of
our HtmlUnit test by using the Page Object Pattern. As mentioned
previously, we can use the Page Object Pattern with HtmlUnit and WebDriver, but it is even easier with Geb. Consider our
new Groovy-based CreateMessagePage implementation:
与直接使用 WebDriver 一样,我们通过使用页面对象模式(Page Object Pattern)改进了我们的 HtmlUnit 测试设计。如前所述,我们可以使用页面对象模式与
HtmlUnit 和 WebDriver 结合,但在 Geb 中操作更为简便。考虑我们基于 Groovy 的新 CreateMessagePage 实现:
Our CreateMessagePage extends Page. We do not go over the details of Page, but, in summary, it contains common
functionality for all of our pages. We define a URL in which this page can be found. This lets us navigate to the page,
as follows:
我们的 CreateMessagePage 扩展了 Page 。我们不详细说明 Page ,但总的来说,它包含了我们所有页面共有的功能。我们定义了一个
URL,可以通过这个 URL 找到这个页面。这使得我们可以按照以下方式导航到该页面:
We also have an at closure that determines if we are at the specified page. It should return true if we are on the
correct page. This is why we can assert that we are on the correct page, as follows:
我们还有一个 at 闭包,用于确定我们是否在指定的页面。它应该返回 true ,如果我们处于正确的页面。这就是为什么我们可以断言我们处于正确的页面,如下所示:
We use an assertion in the closure so that we can determine where things went wrong if we were at the wrong page.
我们在闭包中使用断言,以便在页面错误时确定问题所在。
Next, we create a content closure that specifies all the areas of interest within the page. We can use
a jQuery-ish Navigator API to select the content
in which we are interested.
接下来,我们创建一个指定页面中所有感兴趣区域的 content 闭包。我们可以使用类似 jQuery 的导航器 API 来选择我们感兴趣的内容。
Finally, we can verify that a new message was created successfully, as follows:
最后,我们可以验证一条新消息已成功创建,如下所示:
For further details on how to get the most out of Geb, see The Book of Geb
user’s manual.
有关如何充分利用 Geb 的更多详细信息,请参阅《Geb 用户手册》。