Spring 5 中文解析测试篇-Spring MVC测试框架

微信扫一扫,分享到朋友圈

Spring 5 中文解析测试篇-Spring MVC测试框架

3.6 Spring MVC测试框架

Spring MVC测试框架提供了一流的支持,可使用可与JUnit、TestNG或任何其他测试框架一起使用的流畅API测试Spring MVC代码。它基于 spring-test 模块的 Servlet API模拟对象 构建,因此不使用运行中的Servlet容器。它使用 DispatcherServlet 提供完整的Spring MVC运行时行为,并支持通过 TestContext 框架加载实际的Spring配置以及独立模式,在独立模式下,你可以手动实例化控制器并一次对其进行测试。

Spring MVC Test还为使用 RestTemplate 的代码提供客户端支持。客户端测试模拟服务器响应,并且不使用正在运行的服务器。

Spring Boot提供了一个选项,可以编写包括运行中的服务器在内的完整的端到端集成测试。如果这是你的目标,请参阅《 Spring Boot参考指南 》。有关容器外和端到端集成测试之间的区别的更多信息,请参阅 Spring MVC测试与端到端测试

3.6.1 服务端测试

你可以使用JUnit或TestNG为Spring MVC控制器编写一个普通的单元测试。为此,实例化控制器,向其注入模拟或存根依赖性,然后调用其方法(根据需要传递 MockHttpServletRequestMockHttpServletResponse 等)。但是,在编写这样的单元测试时,仍有许多未经测试的内容:例如,请求映射、数据绑定、类型转换、验证等等。此外,也可以在请求处理生命周期中调用其他控制器方法,例如 @InitBinder@ModelAttribute@ExceptionHandler

Spring MVC Test的目标是通过执行请求并通过实际的 DispatcherServlet 生成响应来提供一种测试控制器的有效方法。Spring MVC Test基于 spring-test 模块中可用的Servlet API的“ 模拟 ”实现。这允许执行请求和生成响应,而无需在Servlet容器中运行。在大多数情况下,一切都应像在运行时一样工作,但有一些值得注意的例外,如 Spring MVC测试与端到端测试 中所述。以下基于JUnit Jupiter的示例使用Spring MVC Test:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.;
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class ExampleTests {
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
void getAccount() throws Exception {
this.mockMvc.perform(get("/accounts/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"))
.andExpect(jsonPath("$.name").value("Lee"));
}
}

Kotlin提供了专用的 MockMvc DSL

前面的测试依赖于 TestContext 框架对 WebApplicationContext 的支持,以从与测试类位于同一包中的XML配置文件加载Spring配置,但是还支持基于Java和基于Groovy的配置。请参阅这些 样本测试

MockMvc实例用于执行对 /accounts/1 的GET请求,并验证结果响应的状态为 200 ,内容类型为 application/json ,响应主体具有名为 name 的JSON属性,其值为 LeeJayway JsonPath 项目支持jsonPath语法。本文档后面将讨论用于验证执行请求结果的许多其他选项。

参考代码: org.liyong.test.annotation.test.spring.WebAppTests

静态导入

上一节中的示例中的流式API需要一些静态导入,例如 MockMvcRequestBuilders.*MockMvcResultMatchers.*MockMvcBuilders.* 。 查找这些类的一种简单方法是搜索与 MockMvc * 相匹配的类型。如果你使用Eclipse或Spring Tools for Eclipse,请确保在Java→编辑器→Content Assist→Favorites下的Eclipse首选项中将它们添加为“ favorite static members ”。这样,你可以在键入静态方法名称的第一个字符后使用内容辅助。其他IDE(例如IntelliJ)可能不需要任何其他配置。检查对静态成员的代码完成支持。

设置选项

你可以通过两个主要选项来创建 MockMvc 实例。第一种是通过 TestContext 框架加载Spring MVC配置,该框架加载Spring配置并将 WebApplicationContext 注入测试中以用于构建 MockMvc 实例。以下示例显示了如何执行此操作:

@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
// ...
}

你的第二个选择是在不加载Spring配置的情况下手动创建控制器实例。而是自动创建基本的默认配置,该配置与MVC JavaConfig 或MVC命名空间大致相当。你可以在一定程度上对其进行自定义。以下示例显示了如何执行此操作:

class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}
// ...
}

你应该使用哪个设置选项?

webAppContextSetup 加载实际的Spring MVC配置,从而进行更完整的集成测试。由于 TestContext 框架缓存了已加载的Spring配置,因此即使你在测试套件中引入更多测试,它也可以帮助保持测试快速运行。此外,你可以通过Spring配置将模拟服务注入控制器中,以继续专注于测试Web层。

下面的示例使用 Mockito 声明一个模拟服务:

<bean id="accountService" factory-method="mock">
<constructor-arg value="org.example.AccountService"/>
</bean>

然后,你可以将模拟服务注入测试中,以设置和验证你的期望,如以下示例所示:

@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {
@Autowired
AccountService accountService;
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
// ...
}

另一方面, standaloneSetup 更接近于单元测试。它一次测试一个控制器。你可以手动注入具有模拟依赖项的控制器,并且不涉及加载Spring配置。这样的测试更多地集中在样式上,并使得查看正在测试哪个控制器,是否需要任何特定的Spring MVC配置等工作变得更加容易。 standaloneSetup 还是编写临时测试以验证特定行为或调试问题的一种非常方便的方法。

与大多数“ 集成与单元测试 ”辩论一样,没有正确或错误的答案。但是,使用 standaloneSetup 确实意味着需要其他 webAppContextSetup 测试,以验证你的Spring MVC配置。另外,你可以使用 webAppContextSetup 编写所有测试,以便始终针对实际的Spring MVC配置进行测试。

设置功能

无论使用哪种 MockMvc 构建器,所有 MockMvcBuilder 实现都提供一些常见且非常有用的功能。例如,你可以为所有请求声明一个 Accept 请求头,并在所有响应中期望状态为200以及 Content-Type 响应头,如下所示:

// static import of MockMvcBuilders.standaloneSetup
MockMvc mockMvc = standaloneSetup(new MusicController())
.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build();

此外,第三方框架(和应用程序)可以预先打包安装说明,例如 MockMvcConfigurer 中的安装说明。Spring框架具有一个这样的内置实现,可帮助保存和重用跨请求的HTTP会话。你可以按以下方式使用它:

// static import of SharedHttpSessionConfigurer.sharedHttpSession
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
.apply(sharedHttpSession())
.build();
// Use mockMvc to perform requests...

有关所有 MockMvc 构建器功能的列表,请参阅 ConfigurableMockMvcBuilder 的javadoc,或使用IDE探索可用选项。

执行请求

你可以使用任何HTTP方法执行请求,如以下示例所示:

mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));

你还可以执行内部使用 MockMultipartHttpServletRequest 的文件上载请求,以便不对 multipart 请求进行实际解析。相反,你必须将其设置为类似于以下示例:

mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));

你可以使用URI模板样式指定查询参数,如以下示例所示:

mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));

你还可以添加代表查询或表单参数的 Servlet 请求参数,如以下示例所示:

mockMvc.perform(get("/hotels").param("thing", "somewhere"));

如果应用程序代码依赖 Servlet 请求参数并且没有显式检查查询字符串(通常是这种情况),则使用哪个选项都没有关系。但是请记住,随URI模板提供的查询参数已被解码,而通过 param(...) 方法提供的请求参数已经被解码。

在大多数情况下,最好将上下文路径和 Servlet 路径保留在请求URI之外。如果必须使用完整的请求URI进行测试,请确保相应地设置 contextPathservletPath ,以便请求映射起作用,如以下示例所示:

mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))

在前面的示例中,为每个执行的请求设置 contextPathservletPath 将很麻烦。相反,你可以设置默认请求属性,如以下示例所示:

class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = standaloneSetup(new AccountController())
.defaultRequest(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build();
}
}

前述属性会影响通过 MockMvc 实例执行的每个请求。如果在给定请求上也指定了相同的属性,则它将覆盖默认值。这就是默认请求中的HTTP方法和URI无关紧要的原因,因为必须在每个请求中都指定它们。

定义期望

你可以通过在执行请求后附加一个或多个 .andExpect(..) 调用来定义期望,如以下示例所示:

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

MockMvcResultMatchers.* 提供了许多期望,其中一些期望与更详细的期望进一步嵌套。

期望分为两大类。第一类断言验证响应的属性(例如,响应状态,标头和内容)。这些是要断言的最重要的结果。

第二类断言超出了响应范围。这些断言使你可以检查Spring MVC的特定切面,例如哪种控制器方法处理了请求、是否引发和处理了异常、模型的内容是什么、选择了哪种视图,添加了哪些刷新属性等等。它们还使你可以检查 Servlet 的特定切面,例如请求和会话属性。

以下测试断言绑定或验证失败:

mockMvc.perform(post("/persons"))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));

很多时候,编写测试时,转储已执行请求的结果很有用。你可以按照以下方式进行操作,其中 print() 是从 MockMvcResultHandlers 静态导入的:

mockMvc.perform(post("/persons"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));

只要请求处理不会引起未处理的异常, print() 方法会将所有有效的结果数据打印到 System.out 。还有一个 log() 方法和 print() 方法的两个其他变体,一个变体接受 OutputStream ,另一个变体接受 Writer 。例如,调用 print(System.err) 将结果数据打印到 System.err ,而调用 print(myWriter) 将结果数据打印到自定义 Writer 。如果要记录而不是打印结果数据,则可以调用 log() 方法,该方法将结果数据作为单个 DEBUG 消息记录在 org.springframework.test.web.servlet.result 记录类别下。

在某些情况下,你可能希望直接访问结果并验证否则无法验证的内容。可以通过在所有其他期望之后附加 .andReturn() 来实现,如以下示例所示:

MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...

如果所有测试都重复相同的期望,则在构建 MockMvc 实例时可以一次设置通用期望,如以下示例所示:

standaloneSetup(new SimpleController())
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build()

请注意,通常会应用共同的期望,并且在不创建单独的 MockMvc 实例的情况下不能将其覆盖。

当JSON响应内容包含使用 Spring HATEOAS 创建的超媒体链接时,可以使用 JsonPath 表达式来验证结果链接,如以下示例所示:

mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));

当XML响应内容包含使用 Spring HATEOAS 创建的超媒体链接时,可以使用 XPath 表达式来验证生成的链接:

Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
.andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));

异步请求

Spring MVC支持的Servlet 3.0异步请求通过存在Servlet容器线程并允许应用程序异步计算响应来工作,然后进行异步调度以完成对Servlet容器线程的处理。

在Spring MVC Test中,可以通过以下方法测试异步请求:首先声明产生的异步值,然后手动执行异步分派,最后验证响应。以下是针对返回 DeferredResultCallable 或Reactor Mono 等反应类型的控制器方法的示例测试:

@Test
void test() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(get("/path"))
.andExpect(status().isOk()) //1
.andExpect(request().asyncStarted()) //2
.andExpect(request().asyncResult("body")) //3
.andReturn();
this.mockMvc.perform(asyncDispatch(mvcResult)) //4
.andExpect(status().isOk()) //5
.andExpect(content().string("body"));
}
  1. 检查响应状态仍然不变
  2. 异步处理必须已经开始
  3. 等待并声明异步结果
  4. 手动执行ASYNC调度(因为没有正在运行的容器)
  5. 验证最终响应

响应流

Spring MVC Test中没有内置选项可用于无容器测试流响应。利用Spring MVC流选项的应用程序可以使用 WebTestClient 对运行中的服务器执行端到端的集成测试。Spring Boot也支持此功能,你可以在其中使用 WebTestClient 测试正在运行的服务器。另一个优势是可以使用Reactor项目中的 StepVerifier 的功能,该功能可以声明对数据流的期望。

注册过滤器

设置 MockMvc 实例时,可以注册一个或多个Servlet Filter 实例,如以下示例所示:

mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

spring-test 通过 MockFilterChain 调用已注册的过滤器,最后一个过滤器委托给 DispatcherServlet

Spring MVC测试与端到端测试

Spring MVC Test基于 spring-test 模块的Servlet API模拟实现而构建,并且不依赖于运行中的容器。因此,与使用实际客户端和实时服务器运行的完整端到端集成测试相比,存在一些差异。

考虑这一点的最简单方法是从一个空白的 MockHttpServletRequest 开始。你添加到其中的内容就是请求的内容。可能令你感到惊讶的是,默认情况下没有上下文路径。没有 jsessionid cookie ;没有转发、错误或异步调度;因此,没有实际的JSP渲染。而是将“ 转发 ”和“重定向” URL保存在 MockHttpServletResponse 中,并且可以按预期进行声明。

这意味着,如果你使用JSP,则可以验证将请求转发到的JSP页面,但是不会呈现HTML。换句话说,不调用JSP。但是请注意,不依赖转发的所有其他渲染技术(例如 ThymeleafFreemarker )都按预期将HTML渲染到响应主体。通过 @ResponseBody 方法呈现 JSONXML 和其他格式时也是如此。

另外,你可以考虑使用 @SpringBootTest 从Spring Boot获得完整的端到端集成测试支持。请参阅《 Spring Boot参考指南 》。

每种方法都有优点和缺点。从经典的单元测试到全面的集成测试,Spring MVC Test中提供的选项在规模上是不同的。可以肯定的是,Spring MVC Test中的所有选项都不属于经典单元测试的类别,但与之接近。例如,你可以通过将模拟服务注入到控制器中来隔离Web层,在这种情况下,你只能通过 DispatcherServlet 并使用实际的Spring配置来测试Web层,因为你可能会与上一层隔离地测试数据访问层。此外,你可以使用独立设置,一次只关注一个控制器,然后手动提供使其工作所需的配置。

使用Spring MVC Test时的另一个重要区别是,从概念上讲,此类测试是服务器端的,因此你可以检查使用了哪个处理程序,如果使用 HandlerExceptionResolver 处理了异常,则模型的内容是什么、绑定错误是什么?还有其他细节。这意味着编写期望值更容易,因为服务器不是黑盒,就像通过实际的HTTP客户端进行测试时一样。通常,这是经典单元测试的优点:它更容易编写、推理和调试,但不能代替完全集成测试的需要。同时,重要的是不要忽略响应是最重要的检查事实。简而言之,即使在同一项目中,这里也存在多种测试样式和测试策略的空间。

更多例子

框架自己的测试包括 许多示例测试 ,旨在展示如何使用Spring MVC Test。你可以浏览这些示例以获取进一步的想法。另外, spring-mvc-showcase 项目具有基于Spring MVC Test的完整测试范围。

3.6.2 HtmlUnit集成

Spring提供了 MockMvcHtmlUnit 之间的集成。使用基于HTML的视图时,这简化了端到端测试的执行。通过此集成你可以:

  • 使用 HtmlUnitWebDriverGeb 等工具可以轻松测试HTML页面,而无需将其部署到Servlet容器中。
  • 在页面中测试JavaScript。

  • (可选)使用模拟服务进行测试以加快测试速度。
  • 在容器内端到端测试和容器外集成测试之间共享逻辑。

MockMvc 使用不依赖Servlet容器的模板技术(例如 ThymeleafFreeMarker 等),但不适用于JSP,因为它们依赖Servlet容器。

为什么集成HtmlUnit

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

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

MockHttpServletRequestBuilder createMessage = post("https://www.tuicool.com/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"));

如果我们要测试允许我们创建消息的表单视图怎么办?例如,假设我们的表单类似于以下代码段:

<form id="messageForm" action="https://www.tuicool.com/messages/" method="post">
<div><a href="https://www.tuicool.com/messages/">Messages</a></div>
<label for="summary">Summary</label>
<input type="text" id="summary" name="summary" value="" />
<label for="text">Message</label>
<textarea id="text" name="text"></textarea>
<div>
<input type="submit" value="Create" />
</div>
</form>

如何确保表单生成创建新消息的正确请求?一个幼稚的尝试可能类似于下面:

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

此测试有一些明显的缺点。如果我们更新控制器以使用参数消息而不是文本,则即使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("https://www.tuicool.com/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"));

这样可以减少我们的测试错误通过的风险,但是仍然存在一些问题:

JavaScript

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

集成测试可以起到补救作用?

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

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

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

要设置这些测试,我们需要确保我们的数据库包含正确的消息。这带来了许多其他挑战:

  • 确保数据库中包含正确的消息可能很繁琐。 (考虑外键约束。)
  • 测试可能会变慢,因为每次测试都需要确保数据库处于正确的状态。
  • 由于我们的数据库需要处于特定状态,因此我们无法并行运行测试。
  • 对诸如自动生成的ID,时间戳等项目进行断言可能很困难。

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

进入HtmlUnit集成

那么,如何在测试页面的交互性之间保持平衡,并在测试套件中保持良好的性能呢?答案是:通过将 MockMvcHtmlUnit 集成。

HtmlUnit集成选项

要将 MockMvcHtmlUnit 集成时,可以有多种选择:

  • MockMvc和HtmlUnit :如果要使用原始的 HtmlUnit 库,请使用此选项。
  • MockMvc和WebDriver :使用此选项可以简化集成和端到端测试之间的开发和重用代码。
  • MockMvc和Geb :如果要使用Groovy进行测试,简化开发并在集成和端到端测试之间重用代码,请使用此选项。

MockMvc 和 HtmlUnit

本节介绍如何集成 MockMvcHtmlUnit 。如果要使用原始 HtmlUnit 库,请使用此选项。

MockMvc和HtmlUnit设置

首先,请确保你已包含对 net.sourceforge.htmlunithtmlunit 的测试依赖项。为了将 HtmlUnit 与Apache HttpComponents 4.5+一起使用,你需要使用 HtmlUnit 2.18 或更高版本。

我们可以使用 MockMvcWebClientBuilder 轻松创建一个与 MockMvc 集成的HtmlUnit WebClient,如下所示:

WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}

这是使用 MockMvcWebClientBuilder 的简单示例。有关高级用法,请参阅 Advanced MockMvcWebClientBuilder

这样可以确保将引用 localhost 作为服务器的所有URL定向到我们的 MockMvc 实例,而无需真正的HTTP连接。通常,通过使用网络连接来请求其他任何URL。这使我们可以轻松测试CDN的使用。

MockMvc和HtmlUnit用法

现在,我们可以像往常一样使用 HtmlUnit ,而无需将应用程序部署到Servlet容器。例如,我们可以请求视图创建以下消息:

HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");

默认上下文路径为 “” 。或者,我们可以指定上下文路径,如 Advanced MockMvcWebClientBuilder 中所述。

一旦有了对 HtmlPage 的引用,我们就可以填写表格并提交以创建一条消息,如以下示例所示:

HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();

最后,我们可以验证是否已成功创建新消息。以下断言使用 AssertJ 库:

assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");

前面的代码以多种方式改进了我们的 MockMvc 测试。首先,我们不再需要显式验证表单,然后创建类似于表单的请求。相反,我们要求表单,将其填写并提交,从而大大减少了开销。

另一个重要因素是 HtmlUnit 使用Mozilla Rhino引擎来评估 JavaScript 。这意味着我们还可以在页面内测试JavaScript的行为。

有关使用HtmlUnit的其他信息,请参见 HtmlUnit文档

MockMvcWebClientBuilder进阶

在到目前为止的示例中,我们通过基于Spring TestContext 框架为我们加载的 WebApplicationContext 构建一个 WebClient ,以最简单的方式使用了MockMvcWebClientBuilder。在以下示例中重复此方法:

WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}

我们还可以指定其他配置选项,如以下示例所示:

WebClient webClient;
@BeforeEach
void setup() {
webClient = MockMvcWebClientBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
}

或者,我们可以通过分别配置 MockMvc 实例并将其提供给 MockMvcWebClientBuilder 来执行完全相同的设置,如下所示:

MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
webClient = MockMvcWebClientBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();

这比较冗长,但是,通过使用 MockMvc 实例构建 WebClient ,我们可以轻而易举地拥有 MockMvc 的全部功能。

有关创建 MockMvc 实例的其他信息,请参见 安装程序选项

MockMvc和WebDriver

在前面的部分中,我们已经了解了如何将 MockMvc 与原始 HtmlUnit API结合使用。在本节中,我们在Selenium WebDriver 中使用其他抽象使事情变得更加容易。

为什么要使用WebDriver和MockMvc?

我们已经可以使用 HtmlUnit 和MockMvc,那么为什么要使用 WebDriverSelenium WebDriver 提供了一个非常优雅的API,使我们可以轻松地组织代码。为了更好地说明它是如何工作的,我们在本节中探索一个示例。

尽管是 Selenium 的一部分, WebDriver 并不需要Selenium Server来运行测试。

假设我们需要确保正确创建一条消息。测试涉及找到HTML表单输入元素,将其填写并做出各种断言。

这种方法会导致大量单独的测试,因为我们也想测试错误情况。例如,如果只填写表格的一部分,我们要确保得到一个错误。如果我们填写整个表格,那么新创建的消息应在之后显示。

如果将其中一个字段命名为“ summary ”,则我们可能会在测试中的多个位置重复以下内容:

HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);

那么,如果我们将 id 更改为 smmry ,会发生什么?这样做将迫使我们更新所有测试以纳入此更改。这违反了DRY原理,因此理想情况下,我们应将此代码提取到其自己的方法中,如下所示:

public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
setSummary(currentPage, summary);
// ...
}
public void setSummary(HtmlPage currentPage, String summary) {
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
}

这样做可以确保在更改UI时不必更新所有测试。

我们甚至可以更进一步,将此逻辑放在代表我们当前所在的 HtmlPage 的Object中,如以下示例所示:

public class CreateMessagePage {
final HtmlPage currentPage;
final HtmlTextInput summaryInput;
final HtmlSubmitInput submit;
public CreateMessagePage(HtmlPage currentPage) {
this.currentPage = currentPage;
this.summaryInput = currentPage.getHtmlElementById("summary");
this.submit = currentPage.getHtmlElementById("submit");
}
public <T> T createMessage(String summary, String text) throws Exception {
setSummary(summary);
HtmlPage result = submit.click();
boolean error = CreateMessagePage.at(result);
return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
}
public void setSummary(String summary) throws Exception {
summaryInput.setValueAttribute(summary);
}
public static boolean at(HtmlPage page) {
return "Create Message".equals(page.getTitleText());
}
}

以前,此模式称为 页面对象模式 。虽然我们当然可以使用 HtmlUnit 做到这一点,但 WebDriver 提供了一些我们在以下各节中探讨的工具,以使该模式的实现更加容易。

MockMvc和WebDriver设置

要将 Selenium WebDriver 与Spring MVC Test框架一起使用,请确保你的项目包含对 org.seleniumhq.selenium:selenium-htmlunit-driver 的测试依赖项。

我们可以使用 MockMvcHtmlUnitDriverBuilder 轻松创建一个与 MockMvc 集成的 Selenium WebDriver ,如以下示例所示:

WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}

这是使用 MockMvcHtmlUnitDriverBuilder 的简单示例。有关更多高级用法,请参见 Advanced MockMvcHtmlUnitDriverBuilder

前面的示例确保将引用 localhost 作为服务器的所有URL定向到我们的 MockMvc 实例,而无需真正的HTTP连接。通常,通过使用网络连接来请求其他任何URL。这使我们可以轻松测试CDN的使用。

MockMvc和WebDriver的用法

现在,我们可以像往常一样使用 WebDriver ,而无需将应用程序部署到Servlet容器。例如,我们可以请求视图创建以下消息:

CreateMessagePage page = CreateMessagePage.to(driver);

然后,我们可以填写表格并提交以创建一条消息,如下所示:

ViewMessagePage viewMessagePage =
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

通过利用页面对象模式,这可以改善我们的 HtmlUnit 测试的设计。正如我们在“ 为什么要使用WebDriver和MockMvc ?”中提到的那样,我们可以将页面对象模式与 HtmlUnit 一起使用,但使用 WebDriver 则要容易得多。考虑以下 CreateMessagePage 实现:

public class CreateMessagePage
extends AbstractPage { //1
//2
private WebElement summary;
private WebElement text;
//3
@FindBy(css = "input[type=submit]")
private WebElement submit;
public CreateMessagePage(WebDriver driver) {
super(driver);
}
public <T> T createMessage(Class<T> resultPage, String summary, String details) {
this.summary.sendKeys(summary);
this.text.sendKeys(details);
this.submit.click();
return PageFactory.initElements(driver, resultPage);
}
public static CreateMessagePage to(WebDriver driver) {
driver.get("http://localhost:9990/mail/messages/form");
return PageFactory.initElements(driver, CreateMessagePage.class);
}
}
  1. CreateMessagePage 扩展 AbstractPage 。我们不详细介绍 AbstractPage ,但总而言之,它包含我们所有页面的通用功能。例如,如果我们的应用程序具有导航栏,全局错误消息以及其他功能,我们可以将此逻辑放置在共享位置。
  2. 对于HTML页面的每个部分,我们都有一个成员变量有兴趣。这些是 WebElement 类型。 WebDriverPageFactory 让我们删除通过自动解析来自 HtmlUnit 版本的 CreateMessagePage 的大量代码每个 WebElementPageFactory#initElements(WebDriver,Class <T>) 方法通过使用字段名称并查找来自动解析每个 WebElement 按HTML页面中元素的ID或名称。
  3. 我们可以使用 @FindBy 注解覆盖默认的查找行为。我们的示例显示了如何使用 @FindBy
    注释以使用CSS选择器( input [type = submit] )查找提交按钮。

最后,我们可以验证是否已成功创建新消息。以下断言使用AssertJ断言库:

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

我们可以看到 ViewMessagePage 允许我们与自定义域模型进行交互。例如,它公开了一个返回 Message 对象的方法:

public Message getMessage() throws ParseException {
Message message = new Message();
message.setId(getId());
message.setCreated(getCreated());
message.setSummary(getSummary());
message.setText(getText());
return message;
}

然后,我们可以在声明中使用富域对象。

最后,我们一定不要忘记在测试完成后关闭 WebDriver 实例,如下所示:

@AfterEach
void destroy() {
if (driver != null) {
driver.close();
}
}

有关使用 WebDriver 的其他信息,请参阅Selenium WebDriver文档

MockMvcHtmlUnitDriverBuilder进阶

在到目前为止的示例中,我们通过基于Spring TestContext 框架为我们加载的 WebApplicationContext 构建一个 WebDriver ,以最简单的方式使用了 MockMvcHtmlUnitDriverBuilder 。在此重复此方法,如下所示:

WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}

我们还可以指定其他配置选项,如下所示:

WebDriver driver;
@BeforeEach
void setup() {
driver = MockMvcHtmlUnitDriverBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
}

或者,我们可以通过分别配置 MockMvc 实例并将其提供给 MockMvcHtmlUnitDriverBuilder 来执行完全相同的设置,如下所示:

MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();

这更为冗长,但是通过使用 MockMvc 实例构建 WebDriver ,我们可以轻而易举地拥有 MockMvc 的全部功能。

有关创建 MockMvc 实例的其他信息,请参见 安装选项

MockMvc和Geb

在上一节中,我们了解了如何在 WebDriver 中使用 MockMvc 。在本节中,我们使用 Geb 来进行甚至Groovy-er的测试。

为什么选择Geb和MockMvc?

Geb得到了 WebDriver 的支持,因此它提供了许多与 WebDriver 相同的好处 。但是,Geb通过为我们处理一些样板代码使事情变得更加轻松。

MockMvc和Geb设置

我们可以轻松地使用使用 MockMvc 的Selenium WebDriver来初始化Geb浏览器,如下所示:

def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}

这是使用 MockMvcHtmlUnitDriverBuilder 的简单示例。有关更多高级用法,请参见 Advanced MockMvcHtmlUnitDriverBuilder

这样可以确保在服务器上引用本地主机的所有URL都定向到我们的 MockMvc 实例,而无需真正的HTTP连接。通常,通过使用网络连接来请求其他任何URL。这使我们可以轻松测试CDN的使用。

MockMvc和Geb用法

现在,我们可以像往常一样使用Geb了,而无需将应用程序部署到Servlet容器中。例如,我们可以请求视图创建以下消息:

 to CreateMessagePage

然后,我们可以填写表格并提交以创建一条消息,如下所示:

when: form.summary = expectedSummary form.text = expectedMessage submit.click(ViewMessagePage)

找不到的所有无法识别的方法调用或属性访问或引用都将转发到当前页面对象。这消除了我们直接使用 WebDriver 时需要的许多样板代码。

与直接使用 WebDriver 一样,这通过使用 Page Object Pattern 改进了 HtmlUnit 测试的设计。如前所述,我们可以将页面对象模式与 HtmlUnitWebDriver 一起使用,但使用Geb则更加容易。考虑我们新的基于Groovy的 CreateMessagePage 实现:

class CreateMessagePage extends Page {
static url = 'messages/form'
static at = { assert title == 'Messages : Create'; true }
static content =  {
submit { $('input[type=submit]') }
form { $('form') }
errors(required:false) { $('label.error, .alert-error')?.text() }
}
}

我们的 CreateMessagePage 扩展了 Page 。我们不会详细介绍 Page ,但是总而言之,它包含了我们所有页面的通用功能。我们定义一个可在其中找到此页面的URL。这使我们可以导航到页面,如下所示:

to CreateMessagePage

我们还有一个at闭包,它确定我们是否在指定页面上。如果我们在正确的页面上,它应该返回 true 。这就是为什么我们可以断言我们在正确的页面上的原因,如下所示:

then:
at CreateMessagePage
errors.contains('This field is required.')

我们在闭包中使用一个断言,以便我们可以确定在错误的页面上哪里出错了。

接下来,我们创建一个内容闭合,该闭合指定页面内所有感兴趣的区域。我们可以使用 jQuery-ish Navigator API 来选择我们感兴趣的内容。

最后,我们可以验证是否已成功创建新消息,如下所示:

then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

有关如何充分利用Geb的更多详细信息,请参见 The Geb Book 用户手册。

3.6.3 客户端REST测试

你可以使用客户端测试来测试内部使用 RestTemplate 的代码。这个想法是声明预期的请求并提供“ 存根 ”响应,以便你可以专注于隔离测试代码(即,不运行服务器)。以下示例显示了如何执行此操作:

RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());
// Test code that uses the above RestTemplate ...
mockServer.verify();

在前面的示例中, MockRestServiceServer (客户端REST测试的中心类)使用自定义的 ClientHttpRequestFactory 配置 RestTemplate ,该 ClientHttpRequestFactory 根据期望断言实际的请求并返回“存根”响应。在这种情况下,我们希望有一个请求 /greeting ,并希望返回200个带有 text/plain 内容的响应。我们可以根据需要定义其他预期的请求和存根响应。当我们定义期望的请求和存根响应时, RestTemplate 可以照常在客户端代码中使用。在测试结束时,可以使用 mockServer.verify() 来验证是否满足所有期望。

默认情况下,请求应按声明的期望顺序进行。你可以在构建服务器时设置 ignoreExpectOrder 选项,在这种情况下,将检查所有期望值(以便)以找到给定请求的匹配项。这意味着允许请求以任何顺序出现。以下示例使用 ignoreExpectOrder

server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

即使默认情况下无顺序请求,每个请求也只能执行一次。 Expect 方法提供了一个重载的变量,该变量接受一个 ExpectedCount 参数,该参数指定一个计数范围(例如, oncemanyTimes ,、 maxminbetween 之间等等)。以下示例使用 times

RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());
// ...
mockServer.verify();

请注意,如果未设置 ignoreExpectOrder (默认设置),并且因此要求按声明顺序进行请求,则该顺序仅适用于任何预期请求中的第一个。例如,如果期望“ /something ”两次,然后是“/somewhere”三次,那么在请求“ /somewhere ”之前应该先请求“ /something ”,但是除了随后的“ /something ”和“ /somewhere ”,请求可以随时发出。

作为上述所有方法的替代,客户端测试支持还提供了 ClientHttpRequestFactory 实现,你可以将其配置为 RestTemplate 以将其绑定到 MockMvc 实例。这样就可以使用实际的服务器端逻辑来处理请求,而无需运行服务器。以下示例显示了如何执行此操作:

MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));
// Test code that uses the above RestTemplate ...

静态导入

与服务器端测试一样,用于客户端测试的流利API需要进行一些静态导入。通过搜索 MockRest 可以轻松找到这些内容。 Eclipse用户应在Java→编辑器→内容辅助→收藏夹下的Eclipse首选项中,将 MockRestRequestMatchersMockRestResponseCreators 。添加为“收藏的静态成员”。这样可以在键入静态方法名称的第一个字符后使用内容辅助。其他IDE(例如IntelliJ)可能不需要任何其他配置。检查是否支持静态成员上的代码完成。

客户端REST测试的更多示例

Spring MVC Test自己的测试包括客户端REST测试的 示例测试

微信公众号:

技术交流群:

中美科技「脱钩」或将成为现实:除了GitHub,中国程序员还应该知道这些代码托管平台

上一篇

kafka数据如何被重复消费

下一篇

你也可能喜欢

Spring 5 中文解析测试篇-Spring MVC测试框架

长按储存图像,分享给朋友