SpringNotes

第 14 章:测试

zjc 于 2026-01-14 发布

这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 测试的目标是用可重复的方式证明行为正确,并在修改时及时暴露回归。Spring 项目测试的关键是分层:单元测试快而稳定,集成测试验证装配和外部契约,端到端测试少量覆盖关键链路。

14.1 测试金字塔

       E2E
      /   \
   API / 集成
  /         \
      单元测试
目标 速度
单元测试 业务逻辑和领域规则
切片测试 Web、Data、JSON 等局部 较快
集成测试 容器装配、数据库、消息
E2E 用户关键路径 最慢

数量建议:单元测试最多,集成测试适量,E2E 少而稳定。

14.2 测试依赖

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

常用库:

用途
JUnit 5 测试框架
AssertJ 断言
Mockito 替身
JSONPath JSON 断言
Testcontainers 真实依赖容器
Awaitility 异步断言
ArchUnit 架构约束

14.3 单元测试

被测类:

public class DiscountCalculator {
    public BigDecimal calculate(UserLevel level, BigDecimal amount) {
        return switch (level) {
            case VIP -> amount.multiply(new BigDecimal("0.85"));
            case NORMAL -> amount;
        };
    }
}

测试:

class DiscountCalculatorTest {

    @Test
    void should_apply_vip_discount() {
        DiscountCalculator calculator = new DiscountCalculator();

        BigDecimal result = calculator.calculate(UserLevel.VIP, new BigDecimal("100.00"));

        assertThat(result).isEqualByComparingTo("85.00");
    }
}

单元测试不需要启动 Spring。大量 @SpringBootTest 会拖慢构建,也会让失败定位变模糊。

14.4 Mockito

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    OrderRepository repository;

    @Test
    void should_create_order() {
        when(repository.nextId()).thenReturn(1L);
        OrderService service = new OrderService(repository);

        Order order = service.create(new CreateOrderCommand("u1", 2, 1));

        assertThat(order.status()).isEqualTo(OrderStatus.CREATED);
        verify(repository).save(any(Order.class));
    }
}

原则:

  1. 不 mock 值对象;
  2. 不过度验证所有调用;
  3. 测试行为而不是实现细节;
  4. 不用 mock 代替设计问题;
  5. 异步逻辑用 Awaitility 验证。

14.5 Web 切片测试

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    OrderService orderService;

    @Test
    void should_return_order() throws Exception {
        when(orderService.get(1L)).thenReturn(new OrderView(1L, "CREATED"));

        mockMvc.perform(get("/api/orders/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.status").value("CREATED"));
    }
}

切片测试只加载 Web 相关 Bean,比完整启动更快,适合验证:

  1. 参数绑定;
  2. 校验;
  3. 状态码;
  4. JSON 序列化;
  5. 异常处理;
  6. 安全规则。

Spring Boot 新版本对 mock Bean 注解有调整,以当前测试 API 为准。

14.6 Testcontainers

@SpringBootTest
@Testcontainers
class OrderIntegrationTests {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    void should_create_and_query_order() {
        OrderView created = restTemplate.postForObject(
                "/api/orders", new CreateOrderRequest("u1", 1L, 1), OrderView.class);

        assertThat(created).isNotNull();
    }
}

优点:

  1. 使用真实数据库;
  2. 避免 H2 与 MySQL 方言差异;
  3. 验证 SQL 和事务;
  4. 可复现环境。

注意:

  1. 本机需要容器运行时;
  2. CI 需要缓存镜像;
  3. 容器启动有成本;
  4. 测试数据要隔离;
  5. 不应每个方法都重建容器。

14.7 数据库测试

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryTests {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");

    @Autowired
    OrderRepository repository;

    @Test
    void should_find_by_user() {
        repository.save(new OrderEntity("u1", OrderStatus.CREATED));

        List<OrderEntity> orders = repository.findByUserId("u1");

        assertThat(orders).hasSize(1);
    }
}

事务回滚策略:

注解 默认行为
@DataJpaTest 每个测试后回滚
@SpringBootTest 不自动回滚

共享容器的集成测试要显式清理数据,避免测试顺序耦合。

14.8 契约测试

生产者验证契约:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProducerContractTests {
}

消费者使用 mock server 验证:

@AutoConfigureMockMvc
class ConsumerTests {
}

Spring Cloud Contract 的价值在于:

  1. 服务双方共享契约;
  2. 提前发现接口破坏;
  3. 消费者测试不依赖生产者部署;
  4. CI 阻止不兼容变更。

契约测试不能替代少量 E2E,但能显著减少跨服务联调成本。

14.9 测试配置

测试配置文件:

spring:
  datasource:
    url: jdbc:tc:mysql:8.4:///order

Profile:

@ActiveProfiles("test")

建议:

  1. 不访问真实生产资源;
  2. 不依赖外部服务状态;
  3. 测试数据可重复构造;
  4. 不依赖执行顺序;
  5. 时间、随机数可注入;
  6. 网络调用使用 WireMock 或容器;
  7. 敏感信息不写入测试代码。

14.10 CI 与质量门禁

Maven:

mvn test
mvn verify

Gradle:

gradle test
gradle check

门禁建议:

指标 建议
单元测试成功率 100%
核心业务行覆盖 按模块设定
集成测试 关键路径必跑
契约变更 双方确认
ArchUnit 架构规则
依赖漏洞 自动扫描
测试耗时 设置预算

覆盖率是结果指标,不是目标。为了覆盖率写无意义测试反而降低质量。

本章小结

Spring 测试应分层:领域和 Service 用快速单元测试,Web/Data 用切片测试,容器装配和外部依赖用 Testcontainers 集成测试,服务间契约用契约测试,端到端只保留关键路径。测试要稳定、可重复、与生产环境相似,并纳入 CI 门禁。

思考题

  1. 什么时候不应使用 @SpringBootTest
  2. Mock 和 Testcontainers 各解决什么问题?
  3. @DataJpaTest 的默认事务行为是什么?
  4. 契约测试解决微服务什么痛点?
  5. 如何保持 CI 测试速度快?