欢迎阅读我的文章!更多精彩内容,欢迎关注:
• B站主页
:小枫Geek
• 微信公众号:Procode  


软件开发里有一句老话:“没有测试的代码等于没有写。”Spring Boot 提供了一套完整的测试支持体系,但很多开发者要么只停留在最简单的 @SpringBootTest,要么写了点单测,却发现和真实场景差距巨大。结果就是——测试环境一片绿油油,到了生产环境照样“翻车”。

这篇文章就带大家系统梳理 Spring Boot 项目测试中最容易踩的坑,以及如何用 MockMvc、Testcontainers 和合理的单元测试姿势,真正做到让测试为开发保驾护航。


1、为什么测试经常失效?

在深入框架之前,先来看一个问题:为什么有些单元测试总是无效?

常见原因有三类:

  1. 测试覆盖不足很多开发者写的“单元测试”其实是“验证 Controller 能否启动”。测试类里一个 @SpringBootTest,然后调用一下方法就完事。这种测试基本只验证了 Spring Boot 能否启动,根本没覆盖到业务逻辑。

  2. 与真实环境差距过大例如测试时用的是 H2 内存数据库,生产上却是 MySQL;测试时直接 Mock Service,结果生产环境里事务配置不生效。最后的效果就是“测什么都过,线上一部署就挂”。

  3. 缺乏可维护性单测写起来一时爽,但随着业务增加,很多测试代码变得冗长、难以维护。于是团队干脆放弃测试,项目测试形同虚设。

要解决这些问题,就要正确地使用 Spring Boot 提供的测试工具。

2、MockMvc:专治 Controller 层的“不服气”

很多同学测试 Controller 时喜欢写 Postman 脚本,或者手动调用接口。这样测试成本极高,根本无法持续集成。 Spring Boot 提供的 MockMvc 就是 Controller 层的神器。

1. 基本用法

假设我们有一个简单的接口:

@RestController
@RequestMapping("/api/user")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return new User(id, "张三");
    }
}

我们可以用 MockMvc 来写测试:

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testGetUser() throws Exception {
        mockMvc.perform(get("/api/user/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("张三"));
    }
}

这里我们做了两件事:

  • 模拟请求 GET /api/user/1。

  • 断言返回的 name 字段等于“张三”。

这样一来,就不需要真的启动服务,也不需要写前端,只测试 Controller 层逻辑。

2. 常见坑点

  • MockMvc 不会加载完整的 Servlet 容器这意味着你在 Controller 里写的某些依赖 Tomcat 的逻辑可能不会被覆盖。

  • 和安全框架结合时要额外配置如果项目启用了 Spring Security,需要额外使用 @WithMockUser 注解,或者配置全局安全策略,否则接口会报 403。

3、Testcontainers:数据库测试不再“将就”

数据库是 Spring Boot 项目里最容易踩坑的地方。 常见做法是用 H2 做内存数据库测试,但问题是:H2 和 MySQL 在 SQL 语法、索引、事务上的差异非常大。你在 H2 上跑通的 SQL,可能线上直接报错。

解决方案就是:Testcontainers。

1. 它是什么?

Testcontainers 是一个 Java 库,可以在测试时自动拉起 Docker 容器,例如 MySQL、Redis、Kafka 等。这样测试环境和生产环境就保持一致,再也不用担心“数据库方言不兼容”。

2. 示例代码

@Testcontainers
@SpringBootTest
class UserRepositoryTest {

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

    @Autowired
    private UserRepository userRepository;

    @Test
    void testSaveUser() {
        User user = new User(null, "李四");
        userRepository.save(user);
        assertNotNull(user.getId());
    }
}

这里的流程是:

  1. 启动测试时,Testcontainers 自动拉起一个 MySQL 8.0 容器。

  2. Spring Boot 自动连接这个容器里的数据库。

  3. 测试完毕后,容器自动销毁,不污染本地环境。

3. 性能优化

有人会担心:“每次测试都要拉个容器,岂不是很慢?”Testcontainers 其实有缓存机制,如果本地已经有镜像,就不会重复下载。而且容器可以在多个测试类中共享,大大提升运行速度。

4、单元测试的正确姿势

单元测试不是“写几个断言”那么简单,要想让测试真正发挥作用,有一些最佳实践值得遵循。

1. 测试金字塔原则

  • 单元测试(Unit Test):数量最多,覆盖业务逻辑。

  • 集成测试(Integration Test):数量适中,覆盖数据库、第三方服务。

  • 端到端测试(E2E Test):数量最少,但覆盖核心流程。

很多项目的问题在于“只写了最顶层的端到端测试”,结果跑一次就要 30 分钟,谁也不愿意维护。

2. 避免过度依赖 Spring Context

有些单测完全没必要启动整个 Spring 容器,例如测试一个工具类、DTO 转换器。 这种场景下,普通 JUnit 测试就足够,没必要加 @SpringBootTest。

3. 数据隔离

无论用 H2 还是 Testcontainers,都要保证每个测试之间的数据隔离。 常见做法有:

  • 使用 @Transactional 注解,测试完成后自动回滚事务。

  • 使用 Testcontainers,每次测试自动创建一个干净的数据库实例。

4. 断言要有意义

不要写这种“为了过而过”的断言:

assertEquals(1, 1);

真正有意义的断言应该贴近业务逻辑:

User user = userRepository.findByName("张三");
assertThat(user).isNotNull();
assertThat(user.getAge()).isGreaterThan(18);

5、CI/CD 中的测试实践

单测写完后,如果只是本地运行,依旧没意义。 正确姿势是:将测试融入 CI/CD 流水线。

例如在 GitHub Actions 里配置:

name: Java CI with Maven

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping --silent"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3

    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 21
        uses: actions/setup-java@v2
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Build with Maven
        run: mvn clean verify

这样每次提交代码,都会自动跑一遍单测,确保不会有人把 bug 合并进主分支。

6、总结

Spring Boot 的测试体系并不复杂,但要写好测试,必须做到三点:

  1. Controller 层用 MockMvc,保证接口逻辑可测、可控。

  2. 数据库层用 Testcontainers,避免测试环境和生产环境不一致。

  3. 遵循单元测试最佳实践,保证测试的覆盖率、可维护性和运行效率。

当 MockMvc、Testcontainers 和合理的单测姿势结合起来,你的 Spring Boot 项目就会形成一个坚固的“防火墙”,大大降低线上翻车的概率。

更多推荐