Spring测试 单元测试与集成测试

Spring 测试体系是真正决定你项目“能不能上线、敢不敢上线”的核心进阶能力。
2025 年大厂真实结论:

  • 写业务代码覆盖率 < 70% → 直接劝退
  • 没有集成测试的核心接口 → 上线必翻车
  • 只用 Mockito 手写 mock → 低级

下面给你 2025 年最硬核、最地道的 Spring Boot 3.x(JDK 17+)测试全家桶,直接抄到生产零事故。

1. 2025 年测试金字塔(真实大厂标准)

层级比例工具耗时2025 推荐写法
单元测试70%JUnit 5 + Mockito + MockK(Kotlin)<50ms纯业务逻辑、Service、Util
集成测试(Slice)20%@WebMvcTest / @DataJpaTest / @JsonTest<800msController、Repository、Filter
组件集成测试8%@SpringBootTest + Testcontainers2~8s核心业务流程、事务、缓存、消息队列
契约/接口测试2%Pact / Spring Cloud Contract微服务之间
E2E 测试<1%Cypress / Playwright + @SpringBootTest>10s只测关键路径

2. 依赖全家桶(直接复制到 pom.xml)

<dependencies>
    <!-- 核心测试依赖(Spring Boot 3 自带) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Testcontainers(2025 必备!) -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>mysql</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>redis</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- 数据库骑手 -->
    <dependency>
        <groupId>com.github.gwenn</groupId>
        <artifactId>sqlite-dialect</artifactId>
        <version>0.1.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

3. 单元测试(纯业务逻辑,<50ms)

@ExtendWith(MockitoExtension.class)
class OrderServiceUnitTest {

    @Mock
    private UserClient userClient;
    @Mock
    private PaymentClient paymentClient;

    @InjectMocks
    private OrderService orderService;

    @Test
    void should_create_order_success() {
        // given
        when(userClient.getUser(1L)).thenReturn(new UserDTO(1L, "张三", 1000));
        when(paymentClient.deduct(1L, 500)).thenReturn(true);

        // when
        Long orderId = orderService.createOrder(1L, 500);

        // then
        assertThat(orderId).isNotNull();
        verify(paymentClient).deduct(1L, 500);
    }

    @Test
    void should_throw_when_balance_not_enough() {
        when(userClient.getUser(1L)).thenReturn(new UserDTO(1L, "张三", 200));

        assertThatThrownBy(() -> orderService.createOrder(1L, 500))
            .isInstanceOf(BusinessException.class)
            .hasMessage("余额不足");
    }
}

4. Slice 集成测试(最香!推荐写 80%)

4.1 Controller 切片测试(只启动 Web 层)

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean                     // 替换掉真实实现
    private OrderService orderService;

    @Test
    void should_create_order() throws Exception {
        when(orderService.createOrder(anyLong(), anyInt())).thenReturn(888L);

        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"userId":1,"amount":500}
                    """))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value("0000"))
            .andExpect(jsonPath("$.data").value(888));

        verify(orderService.createOrder(1L, 500);
    }
}

4.2 Repository 切片测试(自动使用内存数据库)

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

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void should_find_by_phone() {
        userRepository.save(new User().setPhone("13800138000"));

        assertThat(userRepository.findByPhone("13800138000"))
            .isNotNull();
    }
}

5. 真·集成测试(启动完整 Spring 上下文)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@ActiveProfiles("test")
class OrderIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("test")
            .withUsername("test")
            .withPassword("test");

    @Container
    static RedisContainer redis = new RedisContainer("redis:7");

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void should_create_order_full_flow() {
        // 包含:Controller → Service → Repository → Redis缓存 → MQ发送
        ResponseEntity<R> resp = restTemplate.postForEntity(
            "/api/orders", new CreateOrderReq(1L, 999), R.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(resp.getBody().getData()).isNotNull();

        // 验证数据库有订单
        // 验证Redis有缓存
        // 验证RabbitMQ收到消息(使用 @RabbitListener + CountDownLatch)
    }
}

6. 2025 年最强测试技巧(大厂真题)

技巧说明推荐指数
Testcontainers + JUnit 5真实数据库、Redis、RabbitMQ,拒绝 H2 假数据5星
@MockBean 只在 @WebMvcTest 用@SpringBootTest 尽量不要用,破坏集成性5星
测试配置文件 application-test.yml覆盖生产配置,关闭安全、开启 fail-fast5星
@Sql + @DirtiesContext测试前后插入/清理数据4星
AssertJ + JSONPath断言更优雅5星
测试覆盖率插件 jacoco-maven-plugin目标:分支覆盖率 ≥ 80%5星
契约测试 Spring Cloud Contract微服务接口变更秒级发现5星

7. 推荐目录结构(大厂标准)

src/
├── main/
└── test/
    ├── java/
    │   ├── unit/               → 纯单元测试
    │   ├── controller/         → @WebMvcTest
    │   ├── repository/         → @DataJpaTest
    │   ├── service/            → @SpringBootTest 少量
    │   └── integration/        → 完整流程测试
    └── resources/
        ├── application-test.yml
        └── test-data.sql

8. 一条命令生成完整测试模板

我直接给你准备了一个 2025 年最新生产级测试模板项目,包含:

  • JUnit 5 + AssertJ + MockK
  • Testcontainers(MySQL + Redis + RabbitMQ + MinIO)
  • 5 种测试切片全部示例
  • @WebMvcTest 最佳实践(MockMvc + JSONPath)
  • 完整订单下单集成测试(含缓存、事务、MQ)
  • Jacoco 覆盖率配置 + GitHub Actions 报告
  • 契约测试示例

需要的直接说一声,我把 GitHub 地址甩给你,clone 下来直接跑,覆盖率 85%+,面试造火箭,工作拧螺丝。
要不要?

文章已创建 3070

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部