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.10.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.0.13</version>
    <scope>test</scope>
</dependency>

3. Spring MVC 测试配置

3.1 在 JUnit 5 中启用 Spring

JUnit 5 通过扩展接口支持与测试框架集成。通过 @ExtendWith 注解指定扩展类即可启用

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

关键点:

  • @ContextConfiguration 加载配置类(如 ApplicationConfig.class
  • 也可使用 XML 配置:@ContextConfiguration(locations={"classpath:config.xml"})
  • @WebAppConfiguration 加载 Web 应用上下文(默认路径 src/main/webapp

3.2 WebApplicationContext 对象

WebApplicationContext 提供了 Web 应用配置,自动加载所有 Bean 和控制器

@Autowired
private WebApplicationContext webApplicationContext;

3.3 模拟 Web 上下文 Bean

MockMvc 封装了所有 Web 应用 Bean,使其可用于测试

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

⚠️ 在 @BeforeEach 中初始化,避免每个测试重复代码。

3.4 验证测试配置

验证上下文加载是否正确:

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

✅ 检查点:

  1. ServletContext 非空
  2. MockServletContext 实例
  3. greetController Bean 存在

4. 编写集成测试

4.1 验证视图名称

测试接口 /homePage 返回正确视图:

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

关键方法解析:

  • perform():发起 GET 请求
  • andDo(print()):打印请求/响应(调试神器)
  • andExpect():验证视图名为 "index"

4.2 验证响应体

测试接口 /greet 返回 JSON 格式数据:

@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();
    
    assertEquals("application/json;charset=UTF-8", mvcResult.getResponse().getContentType());
}

验证点:

  • HTTP 状态 200
  • JSON 中 message 字段值
  • 响应 Content-Type

4.3 发送带路径变量的 GET 请求

测试接口 /greetWithPathVariable/{name}

@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!!!"));
}

✅ 路径变量通过方法参数直接传递,可读性强。

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

测试接口 /greetWithQueryVariable?name={name}

@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!!!"));
}

两种传参方式:

// 方式1:使用 param()
get("/greetWithQueryVariable").param("name", "John Doe")

// 方式2:URI 模板
get("/greetWithQueryVariable?name={name}", "John Doe")

4.5 发送 POST 请求

测试接口 /greetWithPost

@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!!!"));
}

表单数据示例:

this.mockMvc.perform(post("/greetWithPostAndFormData")
  .param("id", "1")
  .param("name", "John Doe"));

5. MockMvc 的局限性

尽管 MockMvc 使用便捷,但存在以下限制:

  1. 非真实网络调用

    • 使用 TestDispatcherServlet 模拟请求处理
    • ❌ 不会建立真实网络连接
    • ❌ 无法测试完整网络栈
  2. 功能支持不全

    • ⚠️ 不支持 HTTP 重定向(如 Spring Boot 的 /error 接口)
    • ⚠️ 部分过滤器/拦截器可能失效

替代方案(真实 HTTP 测试):

@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);
    }
}

✅ 优势:

  • 真实 HTTP 请求/响应
  • 测试覆盖更完整

6. 总结

本文通过实战演示了 Spring 集成测试的核心要点:

  1. 配置关键

    • @ExtendWith(SpringExtension.class) 启用 Spring
    • @WebAppConfiguration 加载 Web 上下文
    • MockMvc 封装测试环境
  2. 测试场景覆盖

    • 视图名称验证
    • JSON 响应体断言
    • 路径变量/查询参数传递
    • POST 请求处理
  3. 局限性认知

    • 明确 MockMvc 的模拟边界
    • 复杂场景考虑真实 HTTP 测试

💡 完整代码示例见 GitHub 仓库


原始标题:Integration Testing in Spring