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属性,其值为Lee。Jayway 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.standaloneSetupMockMvc 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.sharedHttpSessionMockMvc 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等反应类型的控制器办法的示例测试:

@Testvoid 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提供了MockMvc和HtmlUnit之间的集成。应用基于HTML的视图时,这简化了端到端测试的执行。通过此集成你能够:

  • 应用HtmlUnit、WebDriver和Geb等工具能够轻松测试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;@BeforeEachvoid 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;@BeforeEachvoid setup(WebApplicationContext context) {    webClient = MockMvcWebClientBuilder            .webAppContextSetup(context)            .build();}

咱们还能够指定其余配置选项,如以下示例所示:

WebClient webClient;@BeforeEachvoid 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;@BeforeEachvoid 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实例,如下所示:

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

无关应用WebDriver的其余信息,请参阅Selenium WebDriver文档。

MockMvcHtmlUnitDriverBuilder进阶

在到目前为止的示例中,咱们通过基于Spring TestContext 框架为咱们加载的WebApplicationContext构建一个WebDriver,以最简略的形式应用了MockMvcHtmlUnitDriverBuilder。在此反复此办法,如下所示:

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

咱们还能够指定其余配置选项,如下所示:

WebDriver driver;@BeforeEachvoid 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 CreateMessagePageerrors.contains('This field is required.')
咱们在闭包中应用一个断言,以便咱们能够确定在谬误的页面上哪里出错了。

接下来,咱们创立一个内容闭合,该闭合指定页面内所有感兴趣的区域。咱们能够应用jQuery-ish Navigator API来抉择咱们感兴趣的内容。

最初,咱们能够验证是否已胜利创立新音讯,如下所示:

then:at ViewMessagePagesuccess == 'Successfully created a new message'iddatesummary == expectedSummarymessage == 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测试的示例测试。

作者

集体从事金融行业,就任过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就任于某银行负责对立领取零碎建设。本身对金融行业有强烈的喜好。同时也实际大数据、数据存储、自动化集成和部署、散布式微服务、响应式编程、人工智能等畛域。同时也热衷于技术分享创建公众号和博客站点对常识体系进行分享。关注公众号:青年IT男 获取最新技术文章推送!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1...

微信公众号:

技术交换群: