3.6 Spring MVC测试框架
Spring MVC测试框架为测试Spring MVC代码提供了一流的支持,使用流畅的API可以与JUnit、TestNG或任何其他测试框架一起使用。它是在spring-test模块的Servlet API模拟对象上构建的,因此不使用正在运行的Servlet容器。它使用DispatcherServlet来提供完整的Spring MVC运行时行为,并支持使用TestContext框架加载实际的Spring配置,此外还有一个独立模式,在这个模式中,您可以手动实例化控制器并一次测试一个。
Spring MVC Test还为使用RestTemplate的测试代码提供了客户端支持。客户端测试模拟服务器响应,也不使用正在运行的服务器。
注意:Spring Boot提供了一个选项来编写完整的端到端集成测试,其中包括一个正在运行的服务器。如果这是您的目标,请参阅Spring引导参考指南。有关容器外测试和端到端集成测试之间的差异的更多信息,请参见Spring MVC测试和端到端测试。
3.6.1 服务器端测试
您可以使用JUnit或TestNG为Spring MVC控制器编写一个简单的单元测试。为此,实例化控制器,用模拟的或存根的依赖项注入它,并调用它的方法(根据需要传递MockHttpServletRequest、MockHttpServletResponse等)。但是,在编写这样的单元测试时,还有许多测试没有完成:例如,请求映射、数据绑定、类型转换、验证等等。此外,其他控制器方法,如@InitBinder、@ModelAttribute和@ExceptionHandler,也可以作为请求处理生命周期的一部分调用。
Spring MVC测试的目标是通过实际的DispatcherServlet执行请求并生成响应,从而提供一种有效的方法来测试控制器。
Spring MVC测试建立在Spring - Test模块中提供的Servlet API的熟悉的“mock”实现之上。这允许执行请求和生成响应,而不需要在Servlet容器中运行。在大多数情况下,除了一些明显的例外,一切都应该像运行时那样工作,如Spring MVC测试vs端到端测试中所解释的那样。下面这个基于JUnit类木星的例子使用了Spring MVC测试:
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属性,其值为Lee。通过Jayway jsonPath项目支持jsonPath语法。本文档稍后将讨论用于验证所执行请求的结果的许多其他选项
静态导入
前一节示例中的fluent API需要一些静态导入,比如MockMvcRequestBuilders。*,MockMvcResultMatchers。*,MockMvcBuilders。*。查找这些类的一个简单方法是搜索与MockMvc*匹配的类型。如果您使用Eclipse或基于Eclipse的Spring工具套件,请确保将它们作为“最喜欢的静态成员”添加到Java→Editor→Content Assist→Favorites下的Eclipse首选项中。这样做可以让您在输入静态方法名的第一个字符后使用内容辅助。其他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声明了一个模拟服务:
然后,您可以将模拟服务注入到测试中,以设置和验证您的期望,如下面的示例所示:
@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 builder功能的列表,请参阅ConfigurableMockMvcBuilder的javadoc,或者使用IDE查看可用的选项。
执行请求
你可以执行请求使用任何HTTP方法,如下面的例子所示:
mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
您还可以执行内部使用MockMultipartHttpServletRequest的文件上传请求,这样就不需要对多部分请求进行实际的解析。相反,你必须把它设置成类似下面的例子:
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进行测试,请确保相应地设置contextPath和servletPath,以便让请求映射工作,如下面的示例所示:
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
在前面的示例中,为每个执行的请求设置contextPath和servletPath是很麻烦的。相反,你可以设置默认的请求属性,如下面的例子所示:
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并不重要,因为它们必须在每个请求上指定。
Defining Expectations
您可以通过在执行请求后附加一个或多个.andExpect(..)调用来定义期望,如下面的示例所示:
mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
MockMvcResultMatchers.*提供了许多期望,其中一些期望进一步嵌套了更详细的期望。
预期大致可分为两类。第一类断言验证响应的属性(例如,响应状态、标题和内容)。这些是最重要的结果。
第二类断言超出了响应。这些断言允许您检查Spring MVC的特定方面,例如哪个控制器方法处理了请求、是否引发并处理了异常、模型的内容是什么、选择了什么视图、添加了什么flash属性等等。它们还允许您检查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)将结果数据打印到系统。在调用print(myWriter)时,将结果数据打印到自定义写入器。如果希望将结果数据记录下来而不是打印出来,可以调用log()方法,该方法将结果数据作为单个调试消息记录在org.springframework.test.web.servlet下。结果日志类别。
在某些情况下,您可能希望直接访问结果并验证一些无法通过其他方式验证的内容。这可以通过在所有其他期望之后附加.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 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测试中,可以通过首先声明生成的异步值,然后手动执行异步分派,最后验证响应来测试异步请求。下面是控制器方法的一个示例测试,它返回的是DeferredResult、Callable或reactive类型,如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"));
流媒体的反应
对于流响应的无容器测试,Spring MVC测试中没有内置选项。使用Spring MVC流选项的应用程序可以使用WebTestClient对运行中的服务器执行端到端的集成测试。这在Spring Boot中也得到了支持,您可以使用WebTestClient测试正在运行的服务器。一个额外的优点是能够使用project Reactor的StepVerifier,它允许声明对数据流的期望。
过滤器注册
在设置MockMvc实例时,可以注册一个或多个Servlet过滤器实例,如下例所示:
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
注册的过滤器通过来自spring-test的MockFilterChain调用,最后一个过滤器委托给DispatcherServlet。
Spring MVC测试vs端到端测试
Spring MVC测试构建于Spring - Test模块中的Servlet API模拟实现之上,不依赖于正在运行的容器。因此,与运行实际客户机和活动服务器的完整端到端集成测试相比,有一些不同之处。
最简单的方法是从一个空的MockHttpServletRequest开始。您向它添加的内容就是请求的内容。可能会让您吃惊的是,默认情况下没有上下文路径;没有jsessionid饼干;没有转发、错误或异步调度;因此,没有实际的JSP呈现。相反,“转发”和“重定向”的url保存在MockHttpServletResponse中,并且可以使用期望来断言。
这意味着,如果您使用JSP,您可以验证请求被转发到的JSP页面,但是没有呈现HTML。换句话说,JSP不会被调用。但是,请注意,所有其他不依赖于转发的呈现技术,如Thymeleaf和Freemarker,都按预期将HTML呈现给响应体。通过@ResponseBody方法呈现JSON、XML和其他格式也是如此。
或者,您可以考虑使用@SpringBootTest从SpringBoot获得完整的端到端集成测试支持。参见Spring引导参考指南。
每种方法都有利弊。Spring MVC测试中提供的选项是从传统的单元测试到全面集成测试的不同阶段。可以肯定的是,Spring MVC测试中的所有选项都不属于经典单元测试的范畴,但它们更接近于经典单元测试。例如,您可以通过将模拟服务注入到控制器中来隔离web层,在这种情况下,您只能通过DispatcherServlet测试web层,但是要使用实际的Spring配置,因为您可以在与上面的层隔离的情况下测试数据访问层。此外,您还可以使用独立设置,一次只关注一个控制器,并手动提供使其工作所需的配置。
使用Spring MVC测试的另一个重要区别是,从概念上讲,这样的测试是服务器端测试,因此您可以检查使用了什么处理程序,是否使用HandlerExceptionResolver处理了异常,模型的内容是什么,存在哪些绑定错误,以及其他细节。这意味着编写期望更容易,因为服务器不是黑盒,就像通过实际的HTTP客户机测试服务器一样。这通常是经典单元测试的一个优点:它更容易编写、推理和调试,但是不需要完全的集成测试。与此同时,重要的是不要忽略这样一个事实:要检查的最重要的事情是响应。简而言之,即使在同一个项目中,这里也有多种测试风格和策略的空间。
进一步的例子
该框架自己的测试包括许多示例测试,目的是演示如何使用Spring MVC测试。您可以浏览这些示例以获得更多的想法。另外,Spring - MVC -showcase项目有基于Spring MVC测试的完整测试覆盖。
3.6.2. HtmlUnit Integration
Spring提供了MockMvc和HtmlUnit之间的集成。这简化了在使用基于html的视图时执行端到端的测试。这种集成让您:
注意:MockMvc使用不依赖于Servlet容器的模板技术(例如,Thymeleaf、FreeMarker等),但是它不适用于jsp,因为它们依赖于Servlet容器。
为什么HtmlUnit集成?
最明显的问题就是“为什么我需要这个?”最好通过研究一个非常基础的示例应用程序来找到答案。假设您有一个Spring MVC web应用程序,它支持消息对象上的CRUD操作。应用程序还支持对所有消息进行分页。你会如何测试它?
使用Spring MVC测试,我们可以很容易地测试我们是否能够创建一个消息,如下:
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"));
如果我们想要测试让我们创建消息的表单视图,该怎么办?例如,假设我们的表单如下:
我们如何确保表单生成正确的请求来创建新消息?一个幼稚的尝试可能类似如下:
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"));
这将降低我们的测试不正确通过的风险,但仍有一些问题:
总的问题是,测试web页面不涉及单个交互。相反,它是用户如何与web页面交互以及该web页面如何与其他资源交互的组合。例如,表单视图的结果用作用户创建消息的输入。此外,我们的表单视图可能会使用影响页面行为的其他资源,比如JavaScript验证。
集成测试来拯救?
为了解决前面提到的问题,我们可以执行端到端集成测试,但是这有一些缺点。请考虑测试让我们可以对消息进行分页的视图。我们可能需要以下测试:
要设置这些测试,我们需要确保数据库包含正确的消息。这导致了一些额外的挑战:
这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构我们的详细测试来减少端到端集成测试的数量,从而使用运行更快、更可靠且没有副作用的模拟服务。然后,我们可以实现少量真正的端到端集成测试,这些测试验证简单的工作流,以确保所有工作都能正确地协同工作。
进入HtmlUnit集成
那么,我们如何才能在测试页面交互的同时保持测试套件的良好性能呢?答案是:“通过集成MockMvc和HtmlUnit。”
HtmlUnit集成选项
当你想要集成MockMvc和HtmlUnit时,你有很多选择:
MockMvc和HtmlUnit
本节描述如何集成MockMvc和HtmlUnit。如果您想使用原始的HtmlUnit库,请使用此选项。
MockMvc和HtmlUnit设置
首先,确保您已经包含了一个对net.sourceforge.htmlunit:htmlunit的测试依赖。为了在Apache HttpComponents 4.5+中使用HtmlUnit,您需要使用HtmlUnit 2.18或更高。
我们可以使用MockMvcWebClientBuilder轻松创建一个与MockMvc集成的HtmlUnit WebClient,如下所示:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
注意:这是一个使用MockMvcWebClientBuilder的简单示例。有关高级用法,请参阅高级MockMvcWebClientBuilder。
这确保了任何引用localhost作为服务器的URL都被定向到我们的MockMvc实例,而不需要真正的HTTP连接。任何其他URL都是通过使用网络连接来请求的。这使我们能够轻松地测试CDNs的使用。
MockMvc和HtmlUnit的使用
现在我们可以像往常一样使用HtmlUnit,但不需要将应用程序部署到Servlet容器中。例如,我们可以请求视图创建一个消息如下:
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
注意:默认的上下文路径是“”。或者,我们可以指定上下文路径,就像高级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
在目前的示例中,我们已经以最简单的方式使用了MockMvcWebClientBuilder,方法是基于Spring TestContext框架为我们加载的WebApplicationContext构建一个WebClient。这种方法在下面的例子中重复:
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了,为什么还要使用WebDriver呢?Selenium WebDriver提供了一个非常优雅的API,让我们可以轻松地组织代码。为了更好地展示它是如何工作的,我们将在本节中探索一个示例。
注意:尽管WebDriver是Selenium的一部分,但它并不需要Selenium服务器来运行您的测试。
假设我们需要确保消息被正确创建。这些测试包括查找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,如下面的例子所示:
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 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设置
要在Spring MVC测试框架中使用Selenium WebDriver,请确保您的项目包含对org.seleniumhq.selenium: Selenium -htmlunit-driver的测试依赖。
我们可以使用MockMvcHtmlUnitDriverBuilder轻松创建一个与MockMvc集成的Selenium WebDriver,如下面的示例所示:
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
注意:这是一个使用MockMvcHtmlUnitDriverBuilder的简单示例。有关更高级的用法,请参见高级MockMvcHtmlUnitDriverBuilder。
前面的示例确保将引用localhost作为服务器的任何URL定向到我们的MockMvc实例,而不需要真正的HTTP连接。任何其他URL都是通过使用网络连接来请求的。这使我们能够轻松地测试CDNs的使用。
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 {
private WebElement summary;
private WebElement text;
@FindBy(css = "input[type=submit]")
private WebElement submit;
public CreateMessagePage(WebDriver driver) {
super(driver);
}
public T createMessage(Class 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);
}
}
最后,我们可以验证是否成功创建了新消息。下面的断言使用AssertJ断言库:
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
我们可以看到,ViewMessagePage允许我们与自定义域模型进行交互。例如,它公开了一个返回消息对象的方法:
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
在目前的示例中,我们已经以最简单的方式使用了MockMvcHtmlUnitDriverBuilder,方法是基于Spring TestContext框架为我们加载的WebApplicationContext构建一个WebDriver。这一方法在此重复如下:
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实例构建web驱动程序,我们就可以轻松掌握MockMvc的全部功能。
有关创建MockMvc实例的更多信息,请参见设置选项。
MockMvc and Geb
在前一节中,我们了解了如何在WebDriver中使用MockMvc。在本节中,我们使用Geb使测试更加精彩。
为什么是Geb和MockMvc?
Geb是由WebDriver支持的,所以它提供了许多与WebDriver相同的好处。然而,Geb通过为我们处理一些样板代码使事情变得更加简单。
MockMvc和Geb设置
我们可以使用使用MockMvc的Selenium WebDriver轻松初始化Geb浏览器,如下所示:
def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
这是一个使用MockMvcHtmlUnitDriverBuilder的简单示例。有关更高级的用法,请参见高级MockMvcHtmlUnitDriverBuilder。
这确保了任何引用localhost作为服务器的URL都被定向到我们的MockMvc实例,而不需要真正的HTTP连接。任何其他URL是通过正常使用网络连接请求的。这使我们能够轻松地测试CDNs的使用。
MockMvc和Geb的使用
现在我们可以像往常一样使用Geb,但不需要将应用程序部署到Servlet容器中。例如,我们可以请求视图创建一个消息如下:
to CreateMessagePage
然后我们可以填写表单并提交它来创建一条消息,如下所示:
when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)
任何无法识别的方法调用、属性访问或引用都将被转发到当前页对象。这删除了很多直接使用WebDriver时需要的样板代码。
与直接使用WebDriver一样,这通过使用页面对象模式改进了HtmlUnit测试的设计。正如前面提到的,我们可以在HtmlUnit和WebDriver中使用页面对象模式,但在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。我们不讨论页面的细节,但是,总的来说,它包含所有页面的通用功能。我们定义了一个可以找到这个页面的URL。这让我们导航到该页面,如下所示:
to CreateMessagePage
我们还有一个at闭包,它决定我们是否在指定的页面。如果我们在正确的页面上,它应该返回true。这就是为什么我们可以断言我们在正确的页面,如下所示:
then:
at CreateMessagePage
errors.contains('This field is required.')
我们在闭包中使用断言,这样我们就可以确定如果我们在错误的页面上,哪里出了问题。
接下来,我们创建一个内容闭包,它指定页面中所有感兴趣的区域。我们可以使用类似于jquery的Navigator API来选择感兴趣的内容。
最后,我们可以验证新消息创建成功,如下:
then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage
有关如何充分利用Geb的更多细节,请参阅Geb用户手册。
3.6.3 其他客户端测试
您可以使用客户端测试来测试内部使用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,该工厂根据预期断言实际请求并返回“存根”响应。在这种情况下,我们期望一个请求/问候,并希望返回一个包含文本/纯内容的200个响应。我们可以根据需要定义额外的预期请求和存根响应。当我们定义预期的请求和存根响应时,RestTemplate可以像往常一样在客户端代码中使用。在测试结束时,可以使用mockServer.verify()来验证是否满足了所有的期望。
默认情况下,请求是按照声明期望的顺序出现的。您可以在构建服务器时设置ignoreExpectOrder选项,在这种情况下,所有的期望都会被检查(以便)为给定的请求找到一个匹配项。这意味着请求可以以任何顺序出现。下面的例子使用ignoreExpectOrder:
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
即使是默认的无序请求,每个请求也只允许执行一次。expect方法提供了一个重载的变体,它接受一个ExpectedCount参数,该参数指定一个计数范围(例如,一次、多次、max、min、between,等等)。下面的例子使用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 ...
静态导入
与服务器端测试一样,客户端测试的fluent API需要一些静态导入。这些很容易通过搜索MockRest*找到。Eclipse用户应该添加MockRestRequestMatchers。*和MockRestResponseCreators。*作为“最喜欢的静态成员”在Eclipse首选项下的Java→编辑→内容辅助→收藏。这允许在输入静态方法名的第一个字符后使用内容辅助。其他ide(如IntelliJ)可能不需要任何额外的配置。检查是否支持在静态成员上完成代码。
客户端REST测试的进一步示例
Spring MVC测试自己的测试包括客户端REST测试的示例测试。
3.7 WebTestClient
WebTestClient是一个围绕WebClient的瘦shell,使用它来执行请求并公开一个专用的、流畅的API来验证响应。WebTestClient通过使用模拟请求和响应绑定到WebFlux应用程序,或者它可以通过HTTP连接测试任何web服务器。
Kotlin用户:请参阅本节关于WebTestClient的使用。
3.7.1 设置
要创建WebTestClient,您必须从几个服务器设置选项中选择一个。实际上,您可以将WebFlux应用程序配置为绑定或使用URL连接到正在运行的服务器。
绑定到控制器
下面的例子演示了如何创建一个服务器设置来测试一个@Controller:
client = WebTestClient.bindToController(new TestController()).build();
前面的示例加载WebFlux Java配置并注册给定的控制器。通过使用模拟请求和响应对象,在不使用HTTP服务器的情况下测试得到的WebFlux应用程序。在构建器上有更多的方法来定制默认的WebFlux Java配置。
绑定到路由器功能
下面的例子展示了如何从RouterFunction设置服务器:
RouterFunction> route = ... client = WebTestClient.bindToRouterFunction(route).build();
在内部,配置被传递给RouterFunctions.toWebHandler。通过使用模拟请求和响应对象,在不使用HTTP服务器的情况下测试得到的WebFlux应用程序。
绑定到ApplicationContext
下面的例子展示了如何设置一个服务器从Spring配置你的应用程序或它的一些子集:
@SpringJUnitConfig(WebConfig.class) //1
class MyTests {
WebTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { //2
client = WebTestClient.bindToApplicationContext(context).build(); //3
}
}
在内部,配置被传递给WebHttpHandlerBuilder来设置请求处理链。有关更多细节,请参见WebHandler API。通过使用模拟请求和响应对象,在不使用HTTP服务器的情况下测试得到的WebFlux应用程序。
绑定到服务器
下面的服务器设置选项让你连接到一个正在运行的服务器:
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
客户端构建器
除了前面描述的服务器设置选项之外,您还可以配置客户端选项,包括基本URL、缺省标头、客户端过滤器等。这些选项在bindToServer之后很容易获得。对于所有其他配置,您需要使用configureClient()从服务器配置转换到客户端配置,如下所示:
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();
3.7.2章 编写测试
WebTestClient提供了与WebClient相同的API,直到使用exchange()执行请求为止。exchange()之后是一个链式API工作流,用于验证响应。
通常,首先声明响应状态和报头,如下所示:
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
然后指定如何解码和使用响应体:
然后可以对主体使用内置断言。下面的例子展示了一种方法:
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList(Person.class).hasSize(3).contains(person);
您还可以超越内置的断言,创建自己的断言,如下面的示例所示:
import org.springframework.test.web.reactive.server.expectBody
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (e.g. AssertJ)...
});
你也可以退出工作流程,得到如下结果:
EntityExchangeResult result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
当需要用泛型解码目标类型时,查找接受ParameterizedTypeReference而不是接受Class
没有内容
如果响应没有内容(或者您不关心它是否有内容),那么使用Void。类,它确保资源被释放。下面的例子演示了如何做到这一点:
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
或者,如果你想断言没有响应内容,你可以使用类似下面的代码:
client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
JSON内容
当您使用expectBody()时,响应将作为字节[]使用。这对于原始内容断言非常有用。例如,您可以使用JSONAssert来验证JSON内容,如下所示:
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
您还可以使用JSONPath表达式,如下所示:
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
流媒体的反应
要测试无限流(例如“文本/事件流”或“应用程序/流+json”),您需要在响应状态和报头断言之后立即退出链接API(通过使用returnResult),如下面的示例所示:
FluxExchangeResult result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);
现在您可以使用Flux
Flux eventFlux = result.getResponseBody();
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith(p -> ...)
.thenCancel()
.verify();
请求体
当涉及到构建请求时,WebTestClient提供了一个与WebClient相同的API,并且实现主要是一个简单的传递。有关如何准备带有主体的请求(包括提交表单数据、多部分请求等)的示例,请参阅WebClient文档。
4. 进一步的资源
有关测试的更多信息,请参阅以下参考资料: