3.6Spring MVC测试框架

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

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

Spring Boot提供了编写完整的端到端集成测试的选项,包括正在运行的服务器。 如果这是你的目标,请参阅Spring Boot参考页面。 有关容器外和端到端集成测试之间差异的更多信息,请参阅容器外和端到端集成测试之间的差异。

3.6.1 服务端测试

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

Spring MVC Test的目标是通过执行请求并通过实际的DispatcherServlet生成响应来提供测试控制器的有效方法。

Spring MVC Test建立在Spring测试模块中可用的Servlet API的熟悉的“模拟”实现之上。这允许执行请求并生成响应,而无需在Servlet容器中运行。在大多数情况下,一切都应该像在运行时一样工作,但有一些值得注意的例外,如容器外和端到端集成测试之间的差异所述。以下基于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 {

    private 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.parseMediaType("application/json;charset=UTF-8")))
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json"))
            .andExpect(jsonPath("$.name").value("Lee"));
    }
}

上述测试依赖于TestContext框架的WebApplicationContext支持,从位于与测试类相同的包中的XML配置文件加载Spring配置,但也支持基于Java和Groovy的配置。请参阅这些示例测试。

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

静态Imports

上一节示例中的fluent API需要一些静态导入,例如MockMvcRequestBuilders.*,MockMvcResultMatchers.*和MockMvcBuilders.*。查找这些类的简单方法是搜索与MockMvc *匹配的类型。如果你使用Eclipse或基于Eclipse的Spring Tool Suite,请确保在Java→编辑器→内容辅助→收藏夹下的Eclipse首选项中将它们添加为“最喜欢的静态成员”。这样做可以在键入静态方法名称的第一个字符后使用内容辅助。其他IDE(例如IntelliJ)可能不需要任何其他配置。检查静态成员的代码完成支持。

设置选择

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

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("my-servlet-context.xml")
public class MyWebTests {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    // ...

}

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

public class MyWebTests {

    private MockMvc mockMvc;

    @Before
    public 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>

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

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class AccountTests {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Autowired
    private AccountService accountService;

    // ...

}

另一方面,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 Framework有一个这样的内置实现,可以帮助跨请求保存和重用HTTP会话。 你可以按如下方式使用它:

// static import of SharedHttpSessionConfigurer.sharedHttpSession

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
        .apply(sharedHttpSession())
        .build();

// Use mockMvc to perform requests...

执行请求

你可以执行使用任何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是很麻烦的。 相反,你可以设置默认请求属性,如以下示例所示:

public class MyWebTests {

    private MockMvc mockMvc;

    @Before
    public 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的特定方面,例如处理请求的控制器方法,是否引发和处理异常,模型的内容是什么,选择了哪个视图,添加了哪些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。 Spring Framework 4.2引入了一个log()方法和print()方法的另外两个变体,一个接受OutputStream,另一个接受Writer。 例如,调用print(System.err)将结果数据打印到System.err,而调用print(myWriter)将结果数据打印到自定义编写器。 如果要记录结果数据而不是打印结果数据,可以调用log()方法,该方法将结果数据记录为org.springframework.test.web.servlet.result日志记录类别下的单个DEBUG消息。

在某些情况下,你可能希望直接访问结果并验证无法验证的内容。 这可以通过在所有其他期望之后附加.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"));

Filter注册

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

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

注册过滤器通过Spring测试中的MockFilterChain调用,最后一个过滤器委托给DispatcherServlet。

流式响应

Spring MVC Test内置了无流量响应的无容器测试选项。使用Spring MVC流选项的应用程序可以使用WebTestClient对正在运行的服务器执行端到端的集成测试。 Spring Boot也支持这一点,你可以使用WebTestClient测试正在运行的服务器。另一个优点是能够使用项目Reactor中的StepVerifier,它允许声明对数据流的期望。

容器外和端到端集成测试之间的差异

如前所述,Spring MVC Test构建于spring-test模块的Servlet API模拟对象之上,并不使用正在运行的Servlet容器。因此,与实际客户端和服务器运行的完整端到端集成测试相比,存在一些重要差异。

最简单的思考方法是从空白的MockHttpServletRequest开始。无论你添加什么是请求变成什么。可能会让你感到意外的事情是默认情况下没有上下文路径;没有jsessionid cookie;没有转发,错误或异步调度;因此,没有实际的JSP渲染。相反,“转发”和“重定向”的URL保存在MockHttpServletResponse中,并且可以满足期望。

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

或者,你可以考虑使用@WebIntegrationTest从Spring Boot获得完整的端到端集成测试支持。请参阅Spring Boot参考指南。

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

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

进一步的服务器端测试示例

框架自己的测试包括许多样本测试,旨在展示如何使用Spring MVC测试。你可以浏览这些示例以获取更多想法。此外,spring-mvc-showcase项目还具有基于Spring MVC Test的全面测试覆盖率。

3.6.2 HtmlUnit集成

Spring提供了MockMvc和HtmlUnit之间的集成。 这简化了使用基于HTML的视图时执行端到端测试的过程。 这种集成让你:

  • 使用HtmlUnit,WebDriver和Geb等工具轻松测试HTML页面,而无需部署到Servlet容器。

  • 在页面中测试JavaScript。

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

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

MockMvc使用不依赖于Servlet容器的模板技术(例如,Thymeleaf,FreeMarker等),但它不适用于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 Integration

那么我们如何才能在测试页面交互之间取得平衡,并在测试套件中保持良好的性能呢?答案是:“通过将MockMvc与HtmlUnit集成。”

HtmlUnit集成选项

当你想要将MockMvc与HtmlUnit集成时,你有许多选项:

  • MockMvc和HtmlUnit:如果要使用原始HtmlUnit库,请使用此选项。

  • MockMvc和WebDriver:使用此选项可以简化集成和端到端测试之间的开发和重用代码。

  • MockMvc和Geb:如果要在集成和端到端测试之间使用Groovy进行测试,简化开发和重用代码,请使用此选项。

MockMvc和HtmlUnit

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

MockMvc和HtmlUnit设置

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

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

@Autowired
WebApplicationContext context;

WebClient webClient;

@Before
public void setup() {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}

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

MockMvc和HtmlUnit用法

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

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

一旦我们引用了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 Framework为我们加载的WebApplicationContext的WebClient,以最简单的方式使用了MockMvcWebClientBuilder。 在以下示例中重复此方法:

@Autowired
WebApplicationContext context;

WebClient webClient;

@Before
public void setup() {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}

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

WebClient webClient;

@Before
public 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和WebDriver

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

为什么选择WebDriver和MockMvc?

我们已经可以使用HtmlUnit和MockMvc,那么我们为什么要使用WebDriver呢? Selenium WebDriver提供了一个非常优雅的API,可以让我们轻松地组织我们的代码。 为了更好地展示它的工作原理,我们将在本节中探讨一个示例。

假设我们需要确保正确创建消息。 测试涉及查找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时不必更新所有测试。

我们甚至可以更进一步,将此逻辑放在一个Object中,该Object表示我们当前所在的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> 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,如下例所示:

   ​@Autowired
    WebApplicationContext context;

    WebDriver driver;

    @Before
    public void setup() {
        driver = MockMvcHtmlUnitDriverBuilder
                .webAppContextSetup(context)
                .build();
    }

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

MockMvc和WebDriver用法

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

CreateMessagePage page = CreateMessagePage.to(driver);

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

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

这通过利用页面对象模式改进了HtmlUnit测试的设计。 正如我们在Why 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> 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);
    }
}

最后,我们可以验证是否已成功创建新消息。 以下断言使用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实例,如下所示:

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

高级MockMvcHtmlUnitDriverBuilder

在目前为止的示例中,我们通过构建基于Spring TestContext Framework为我们加载的WebApplicationContext的WebDriver,以最简单的方式使用了MockMvcHtmlUnitDriverBuilder。 这里重复这种方法,如下:

@Autowired
WebApplicationContext context;

WebDriver driver;

@Before
public void setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}

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

WebDriver driver;

    @Before
    public 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和Geb

在上一节中,我们了解了如何将MockMvc与WebDriver一起使用。 在本节中,我们使用Geb使我们的测试甚至是Groovy-er。

为什么选择Geb和MockMvc?

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

MockMvc和Geb设置

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

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

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

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。 我们不会详细介绍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

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,该客户端根据预期断言实际请求并返回“存根”响应。 在这种情况下,我们期望对/ greeting发出请求,并希望返回带有text / plain内容的200响应。 我们可以根据需要定义其他预期请求和存根响应。 当我们定义期望的请求和存根响应时,RestTemplate可以像往常一样在客户端代码中使用。 在测试结束时,mockServer.verify()可用于验证是否已满足所有期望。

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

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

即使默认情况下无序请求,每个请求也只允许执行一次。 expect方法提供了一个重载变量,它接受指定计数范围的ExpectedCount参数(例如,once,manyTimes,max,min,between等)。 以下示例使用时间:

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”的请求,但是,除了后续的“/”之外 某些“和”/某处“,请求可以随时出现。

作为上述所有选项的替代方案,客户端测试支持还提供了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首选项中添加MockRestRequestMatchers.*和MockRestResponseCreators。*作为“最喜欢的静态成员”。 这允许在键入静态方法名称的第一个字符后使用内容辅助。 其他IDE(例如IntelliJ)可能不需要任何其他配置。 检查静态成员对代码完成的支持。

客户端REST测试的更多示例

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

最后更新于