Contents

Spring 集成测试

1. 概述

集成测试通过验证系统的端到端行为在应用程序开发周期中发挥着重要作用。

在本教程中,我们将学习如何利用 Spring MVC 测试框架来编写和运行测试控制器的集成测试,而无需显式启动 Servlet 容器。

2. 准备

我们将需要几个 Maven 依赖项来运行我们将在本文中使用的集成测试。首先,我们需要最新的junit-jupiter-enginejunit-jupiter-apiSpring test 依赖项:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.3</version>
    <scope>test</scope>
</dependency>

为了有效地断言结果,我们还将使用Hamcrestjsonpath

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>2.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.5.0</version>
    <scope>test</scope>
</dependency>

3. Spring MVC 测试配置

现在让我们看看如何配置和运行启用 Spring 的测试。

3.1. 使用 JUnit 5 在测试中启用 Spring

JUnit 5 定义了一个扩展接口,通过它类可以与 JUnit 测试集成。

我们可以通过将*@ExtendWith*注解添加到我们的测试类并指定要加载的扩展类来启用此扩展。为了运行 Spring 测试,我们使用SpringExtension.class

我们还需要***@ContextConfiguration*注解来加载上下文配置并引导我们的测试将使用的上下文**。

我们来看一下:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { ApplicationConfig.class })
@WebAppConfiguration
public class GreetControllerIntegrationTest {
    ....
}

请注意,在*@ContextConfiguration 中,我们提供了ApplicationConfig.class*配置类,它加载了我们需要进行此特定测试的配置。

我们将在此处使用 Java 配置类来指定上下文配置。同样,我们可以使用基于 XML 的配置:

@ContextConfiguration(locations={""})

最后,我们还将使用 @WebAppConfiguration 注解测试,这将加载 Web 应用程序上下文。

默认情况下,它会在路径src/main/webapp 中查找根 Web 应用程序。**我们可以通过简单地传递value属性来覆盖这个位置:

@WebAppConfiguration(value = "")

3.2. WebApplicationContext 对象

WebApplicationContext 提供 Web 应用程序配置。它将所有应用程序 bean 和控制器加载到上下文中。

现在我们将能够将 Web 应用程序上下文直接连接到测试中:

@Autowired
private WebApplicationContext webApplicationContext;

3.3. 模拟 Web 上下文 Bean

MockMvc提供对 Spring MVC 测试的支持。它封装了所有 Web 应用程序 bean 并使它们可用于测试。

让我们看看如何使用它:

private MockMvc mockMvc;
@BeforeEach
public void setup() throws Exception {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build();
}

我们将在*@BeforeEach注解方法中初始化mockMvc*对象,这样我们就不必在每个测试中初始化它。

3.4. 验证测试配置

让我们验证我们是否正确加载了WebApplicationContext对象 ( webApplicationContext )。我们还将检查是否附加了正确的servletContext

@Test
public void givenWac_whenServletContext_thenItProvidesGreetController() {
    ServletContext servletContext = webApplicationContext.getServletContext();
    
    Assert.assertNotNull(servletContext);
    Assert.assertTrue(servletContext instanceof MockServletContext);
    Assert.assertNotNull(webApplicationContext.getBean("greetController"));
}

请注意,我们还检查了一个GreetController.java bean 是否存在于 Web 上下文中。这可以确保正确加载 Spring bean。至此,集成测试的设置就完成了。现在,我们将了解如何使用MockMvc对象测试资源方法。

4. 编写集成测试

在本节中,我们将介绍通过测试框架可用的基本操作。

我们将看看如何使用路径变量和参数发送请求。我们还将使用一些示例来说明如何断言正确的视图名称已解析,或者响应正文符合预期。

下面显示的片段使用来自MockMvcRequestBuildersMockMvcResultMatchers类的静态导入。

4.1.验证视图名称

我们可以从我们的测试中调用*/homePage*端点:

http://localhost:8080/spring-mvc-test/

或者

http://localhost:8080/spring-mvc-test/homePage

首先,让我们看一下测试代码:

@Test
public void givenHomePageURI_whenMockMVC_thenReturnsIndexJSPViewName() {
    this.mockMvc.perform(get("/homePage")).andDo(print())
      .andExpect(view().name("index"));
}

让我们分解一下:

  • perform()方法将调用 GET 请求方法,该方法返回ResultActions。使用这个结果,我们可以对响应有断言期望,比如它的内容、HTTP 状态或标头。
  • *andDo(print())*将打印请求和响应。这有助于在发生错误时获得详细视图。
  • andExpect()将期望提供的参数。在我们的例子中,我们期望通过MockMvcResultMatchers.view() 返回“索引”。

4.2. 验证响应正文

我们将从测试中调用*/greet*端点:

http://localhost:8080/spring-mvc-test/greet

预期的输出将是:

{
    "id": 1,
    "message": "Hello World!!!"
}

让我们看看测试代码:

@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() {
    MvcResult mvcResult = this.mockMvc.perform(get("/greet"))
      .andDo(print()).andExpect(status().isOk())
      .andExpect(jsonPath("$.message").value("Hello World!!!"))
      .andReturn();
    
    Assert.assertEquals("application/json;charset=UTF-8", 
      mvcResult.getResponse().getContentType());
}

让我们看看到底发生了什么:

  • andExpect(MockMvcResultMatchers.status().isOk())将验证响应 HTTP 状态是Ok ( 200)。这确保了请求被成功执行。
  • andExpect(MockMvcResultMatchers.jsonPath(“$.message”).value(“Hello World!!!”))将验证响应内容是否与参数“ Hello World!!! ” 这里,我们使用了 jsonPath,它提取响应内容并提供请求的值。
  • andReturn()将返回MvcResult对象,当我们必须验证库无法直接实现的某些内容时使用该对象。在这种情况下,我们添加了assertEquals以匹配从MvcResult对象中提取的响应的内容类型。

4. 3. 发送带有路径变量的 GET 请求

我们将从我们的测试中调用*/greetWithPathVariable/{name}*端点:

http://localhost:8080/spring-mvc-test/greetWithPathVariable/John

预期的输出将是:

{
    "id": 1,
    "message": "Hello World John!!!"
}

让我们看看测试代码:

@Test
public void givenGreetURIWithPathVariable_whenMockMVC_thenResponseOK() {
    this.mockMvc
      .perform(get("/greetWithPathVariable/{name}", "John"))
      .andDo(print()).andExpect(status().isOk())
      
      .andExpect(content().contentType("application/json;charset=UTF-8"))
      .andExpect(jsonPath("$.message").value("Hello World John!!!"));
}

*MockMvcRequestBuilders.get(“/greetWithPathVariable/{name}”, “John”)*将以“ /greetWithPathVariable/John”的形式发送请求。

这在可读性和了解 URL 中动态设置的参数方面变得更容易。请注意,我们可以根据需要传递尽可能多的路径参数。

4.4. 发送带有查询参数的 GET 请求

我们将从测试中调用*/greetWithQueryVariable?name={name}*端点:

http://localhost:8080/spring-mvc-test/greetWithQueryVariable?name=John%20Doe

在这种情况下,预期的输出将是:

{
    "id": 1,
    "message": "Hello World John Doe!!!"
}

现在,让我们看看测试代码:

@Test
public void givenGreetURIWithQueryParameter_whenMockMVC_thenResponseOK() {
    this.mockMvc.perform(get("/greetWithQueryVariable")
      .param("name", "John Doe")).andDo(print()).andExpect(status().isOk())
      .andExpect(content().contentType("application/json;charset=UTF-8"))
      .andExpect(jsonPath("$.message").value("Hello World John Doe!!!"));
}

param(“name”, “John Doe”)将在 GET 请求中附加查询参数。这类似于“/greetWithQueryVariable?name=John%20Doe“。

查询参数也可以使用 URI 模板样式实现:

this.mockMvc.perform(
  get("/greetWithQueryVariable?name={name}", "John Doe"));

4.5. 发送 POST 请求

我们将从测试中调用*/greetWithPost*端点,如下所示:

http://localhost:8080/spring-mvc-test/greetWithPost

我们应该得到输出:

{
    "id": 1,
    "message": "Hello World!!!"
}

我们的测试代码是:

@Test
public void givenGreetURIWithPost_whenMockMVC_thenVerifyResponse() {
    this.mockMvc.perform(post("/greetWithPost")).andDo(print())
      .andExpect(status().isOk()).andExpect(content()
      .contentType("application/json;charset=UTF-8"))
      .andExpect(jsonPath("$.message").value("Hello World!!!"));
}

*MockMvcRequestBuilders.post(“/greetWithPost”)将发送 POST 请求。我们可以像以前一样设置路径变量和查询参数,而表单数据只能通过param()*方法设置,类似于查询参数:

http://localhost:8080/spring-mvc-test/greetWithPostAndFormData

那么数据将是:

id=1;name=John%20Doe

所以我们应该得到:

{
    "id": 1,
    "message": "Hello World John Doe!!!"
}

让我们看看我们的测试:

@Test
public void givenGreetURI_whenMockMVC_thenVerifyResponse() throws Exception {
    MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/greet"))
      .andDo(print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Hello World!!!"))
      .andReturn();
 
   assertEquals("application/json;charset=UTF-8", mvcResult.getResponse().getContentType());
}

在上面的代码片段中,我们添加了两个参数:id为“1”,name为“John Doe”。

5. MockMvc限制

MockMvc提供了一个优雅且易于使用的 API 来调用 Web 端点并同时检查和断言它们的响应。尽管有很多好处,但它也有一些限制。

首先,它确实使用 *DispatcherServlet *的子类来处理测试请求。更具体地说,  *TestDispatcherServlet *负责调用控制器并执行所有熟悉的 Spring 魔术。

MockMvc类在 内部包装 了这个 TestDispatcherServlet。所以每次我们使用perform()方法发送请求时,MockMvc都会直接使用底层的 TestDispatcherServlet。因此,没有建立真正的网络连接,因此,我们不会在使用MockMvc时测试整个网络堆栈。

此外,由于 Spring 准备了一个虚假的 Web 应用程序上下文来模拟 HTTP 请求和响应,它可能不支持成熟的 Spring 应用程序的所有功能

例如,这个模拟设置不支持HTTP 重定向 。起初这似乎并不重要。但是,Spring Boot 通过将当前请求重定向到*/error端点来处理一些错误。因此,如果我们使用MockMvc,*我们可能无法测试一些 API 故障。

作为MockMvc 的替代方案,我们可以设置一个更真实的应用程序上下文,然后使用*RestTemplate *甚至 REST-assured 来测试我们的应用程序。

例如,这很容易使用 Spring Boot:

@SpringBootTest(webEnvironment = DEFINED_PORT)
public class GreetControllerRealIntegrationTest {
    @Before
    public void setUp() {
        RestAssured.port = DEFAULT_PORT;
    }
    @Test
    public void givenGreetURI_whenSendingReq_thenVerifyResponse() {
        given().get("/greet")
          .then()
          .statusCode(200);
    }
}

在这里,我们甚至不需要添加*@ExtendWith(SpringExtension.class)*。

这样,每个测试都会向侦听随机 TCP 端口的应用程序发出真正的 HTTP 请求。