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

用户头像
青年IT男
关注
发布于: 2020 年 09 月 13 日
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测试与端到端测试](https://docs.spring.io/spring/docs/5.2.7.RELEASE/spring-framework-reference/testing.html#spring-mvc-test-vs-end-to-end-integration-tests)。



###### 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" class="org.mockito.Mockito" 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](https://github.com/spring-projects/spring-mvc-showcase)项目具有基于Spring MVC Test的完整测试范围。



###### 3.6.2 HtmlUnit集成



Spring提供了MockMvc和[HtmlUnit](http://htmlunit.sourceforge.net/)之间的集成。使用基于HTML的视图时,这简化了端到端测试的执行。通过此集成你可以:



  • 使用HtmlUnit、[WebDriver](https://www.seleniumhq.org/)和[Geb](http://www.gebish.org/manual/current/#spock-junit-testng)等工具可以轻松测试HTML页面,而无需将其部署到Servlet容器中。



  • 在页面中测试JavaScript。

  • (可选)使用模拟服务进行测试以加快测试速度。

  • 在容器内端到端测试和容器外集成测试之间共享逻辑。



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



为什么集成HtmlUnit



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



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



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"));



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



<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());



此测试有一些明显的缺点。如果我们更新控制器以使用参数消息而不是文本,则即使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, "Spring Rocks")
.param(textParamName, "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));



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



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

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

  • 最后,我们仍然无法解释某些事情。例如,如果表单也具有我们希望测试的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](https://docs.spring.io/spring/docs/5.2.7.RELEASE/spring-framework-reference/testing.html#spring-mvc-test-server-htmlunit-webdriver-why)?”中提到的那样,我们可以将页面对象模式与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();
}



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