关于spring:Spring-5-中文解析测试篇Spring-MVC测试框架

42次阅读

共计 25246 个字符,预计需要花费 64 分钟才能阅读完成。

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.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 提供了 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;

@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 测试的示例测试。

作者

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

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

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

微信公众号:

技术交换群:

正文完
 0