测试 Spring Boot REST API

最后更新: 2026-08-17 10:18:23 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

测试 Spring Boot REST API 需要多个范围,因为直接的 Java 调用、模拟的 MVC 请求以及通过运行中服务器发出的请求,会分别验证不同的故障边界。有效的测试套件应将大多数行为检查放在 Spring 上下文之外,然后再添加范围更窄的 Web 切片测试和少量完整应用测试。

依赖示例面向 Spring Boot 3.x,因为其测试 starter 使用 JUnit 5。Spring Boot 4 管理 JUnit 6;Jupiter 的概念仍然适用,但若干 Boot 测试注解的包名发生了变化。

不使用测试框架进行测试 #

REST API 无需 JUnit、Spring Test 或 Mockito 也可以进行检查。启动应用,使用 JDK HTTP 客户端发送请求,并在响应违反契约时抛出错误:

java
package com.example;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public final class ManualApiCheck {
    public static void main(String[] args) throws Exception {
        var request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080/greetings/Ada"))
                .header("Accept", "application/json")
                .GET()
                .build();

        var response = HttpClient.newHttpClient().send(
                request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new AssertionError("status: " + response.statusCode());
        }
        if (!response.body().equals("{\"message\":\"Hello, Ada\"}")) {
            throw new AssertionError("body: " + response.body());
        }
    }
}

Java 的 assert 语句可以替代显式检查,但除非 JVM 使用 -ea 启动,否则断言处于禁用状态。因此,显式的 AssertionError 能够提供可靠的进程退出代码。

自定义可执行程序仍然适用于对已部署服务执行冒烟检查。完全替代测试框架还需要实现测试发现、fixture 生命周期、隔离、结果报告、并行执行和构建集成。手动的 curl 或浏览器检查提供的是探索能力,而不是可重复的回归测试套件。

选择测试范围 #

Spring 的测试文档区分了单元测试和集成测试,并提供了多个 Web 测试范围。不存在适用于每个端点的单一推荐注解。应选择包含待检查行为的最小范围:

范围机制所验证的行为
普通单元测试JUnit 和普通构造函数不使用 Spring 的控制器或服务 Java 逻辑
MVC 切片@WebMvcTest 和 MockMvc路由、绑定、验证、序列化、过滤器和控制器通知
带模拟 Web 层的完整上下文@SpringBootTest 和 @AutoConfigureMockMvc应用装配以及模拟的 Servlet 请求
完整运行服务器@SpringBootTest(webEnvironment = RANDOM_PORT)嵌入式服务器、过滤器、序列化、网络和完整应用装配
已部署系统测试外部 HTTP 客户端打包后的服务及其已部署的依赖项

Spring 建议在不需要 Spring 容器时使用单元测试。随后,MockMvc 可以在不运行服务器的情况下提供完整的 Spring MVC 请求处理。仍应保留一些运行服务器测试,因为模拟请求无法覆盖 Servlet 容器的网络、分发和错误页面行为。

使用数据库或 broker 的测试需要单独进行依赖决策:

  • 测试应用决策时,将边界替换为 fake 或 mock。
  • 如果数据库查询、映射、事务、broker 行为或线协议兼容性属于需求的一部分,则使用隔离的真实服务。
  • 自动化测试绝不能指向生产基础设施。

Spring Boot 测试依赖 #

已经继承 Spring Boot 3 parent 或导入其 BOM 的应用只需要一个测试依赖:

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

该 starter 包含 Spring Boot Test、Spring Test、JUnit Jupiter、Mockito、AssertJ、Hamcrest、JSONassert、JsonPath 和 Awaitility。由于整个依赖集合作为同一个发行序列进行测试,因此应让 Spring Boot 管理它们的版本。

示例使用以下应用边界:

java
public interface GreetingService {
    Greeting greet(String name);
}

public record Greeting(String message) {}

@RestController
@RequestMapping("/greetings")
public final class GreetingController {
    private final GreetingService service;

    public GreetingController(GreetingService service) {
        this.service = service;
    }

    @GetMapping("/{name}")
    Greeting get(@PathVariable String name) {
        return service.greet(name);
    }
}

普通 JUnit 5 测试 #

Spring 组件仍然是普通 Java 对象。当测试关注分支、返回值或服务委托时,直接构造控制器可以避免启动上下文:

java
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class GreetingControllerTest {
    @Test
    void returnsTheServiceResult() {
        GreetingService service =
                name -> new Greeting("Hello, " + name);
        var controller = new GreetingController(service);

        assertEquals(
                new Greeting("Hello, Ada"),
                controller.get("Ada"));
    }
}

该测试不会验证 @GetMapping、路径变量转换、JSON 序列化、验证、过滤器或异常处理器,因为直接调用方法绕过了 Spring MVC。

使用 JUnit 5 的 MVC 切片测试 #

@WebMvcTest 会加载 MVC 基础设施和选定的 Web 组件,而不是完整应用。为控制器依赖提供确定性实现,从而将 Mockito 排除在测试之外:

java
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
@Import(GreetingControllerWebTest.Dependencies.class)
class GreetingControllerWebTest {
    @Autowired
    MockMvc mvc;

    @Test
    void rendersAJsonGreeting() throws Exception {
        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(
                        "application/json"))
                .andExpect(jsonPath("$.message").value("Hello, Ada"));
    }

    @TestConfiguration(proxyBeanMethods = false)
    static class Dependencies {
        @Bean
        GreetingService greetingService() {
            return name -> new Greeting("Hello, " + name);
        }
    }
}

当安全配置、控制器通知、转换器或过滤器的行为属于端点契约时,应将它们添加到切片中。排除了待测生产组件的切片可能在组装后的应用失败时仍然通过,因此完整上下文覆盖仍然是必要的。

完整上下文测试 #

默认的 @SpringBootTest Web 环境是 MOCK。添加 @AutoConfigureMockMvc,即可通过完整应用上下文执行请求,而无需打开网络端口:

java
@SpringBootTest
@AutoConfigureMockMvc
class GreetingApplicationTest {
    @Autowired
    MockMvc mvc;

    @Test
    void servesAGreeting() throws Exception {
        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Hello, Ada"));
    }
}

当嵌入式服务器或真实 HTTP 行为属于测试范围时,应使用随机端口环境:

java
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingHttpTest {
    @LocalServerPort
    int port;

    @Test
    void servesAGreetingOverHttp() throws Exception {
        var request = HttpRequest.newBuilder()
                .uri(URI.create(
                        "http://localhost:" + port + "/greetings/Ada"))
                .GET()
                .build();

        var response = HttpClient.newHttpClient().send(
                request, HttpResponse.BodyHandlers.ofString());

        assertEquals(200, response.statusCode());
        assertEquals(
                "{\"message\":\"Hello, Ada\"}",
                response.body());
    }
}

随机端口可以防止并发构建期间发生固定端口冲突。客户端和服务器代码运行在不同线程中,因此测试线程启动的事务不会自动回滚运行服务器请求提交的数据。应显式重置状态,或隔离后端服务。

Boot 3 还提供 TestRestTemplate,而 Spring 的响应式栈提供 WebTestClient。Spring Framework 7 添加了 RestTestClient,用于在 MockMvc 和运行服务器测试之间提供通用的同步断言 API。应选择应用所使用 Spring 版本支持的客户端,而不是直接复制未注明版本的示例。

不使用 Spring 上下文的 JUnit 5 和 Mockito #

当 Spring 配线不属于测试对象时,使用 MockitoExtension:

java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class GreetingControllerMockitoTest {
    @Mock
    GreetingService service;

    @InjectMocks
    GreetingController controller;

    @Test
    void delegatesToTheService() {
        when(service.greet("Ada"))
                .thenReturn(new Greeting("Welcome, Ada"));

        assertEquals(
                new Greeting("Welcome, Ada"),
                controller.get("Ada"));
        verify(service).greet("Ada");
    }
}

该测试不会加载 Spring 上下文。@InjectMocks 会调用控制器构造函数,而 @Mock 会创建普通的 Mockito 对象。

Spring 测试上下文中的 Mockito #

普通的 @Mock 字段不会替换 Spring ApplicationContext 中的 Bean。当切片测试或完整上下文测试需要覆盖 Bean 时,应使用 @MockitoBean:

java
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
class GreetingControllerMockitoWebTest {
    @Autowired
    MockMvc mvc;

    @MockitoBean
    GreetingService service;

    @Test
    void rendersTheMockedServiceResult() throws Exception {
        when(service.greet("Ada"))
                .thenReturn(new Greeting("Welcome, Ada"));

        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Welcome, Ada"));

        verify(service).greet("Ada");
    }
}

Spring Framework 的 TestContext 支持会创建 mock,并在刷新上下文之前替换匹配的 Bean。Spring Boot 测试注解已经注册了 SpringExtension,因此添加 MockitoExtension 无助于替换上下文 Bean。

当必须保留选定的真实行为时,使用 @MockitoSpyBean 包装现有的上下文 Bean。Spring Boot 较旧的 @MockBean 和 @SpyBean 注解在 Boot 3.4 中已弃用,改为推荐使用 Spring Framework 的 @MockitoBean 和 @MockitoSpyBean。使用 Boot 3.3 或更早版本的项目必须使用旧注解。

应将 Bean 覆盖应用于远程客户端、消息发布器、时钟或 repository 等架构边界。在完整上下文测试中替换大量内部 Bean,会移除选择 @SpringBootTest 所要验证的配线;此时,普通单元测试或 MVC 切片能够更准确地表达测试范围。

参考资料 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)