学完这篇,你能从零把 JUnit 5、Mockito、Testcontainers 三层测试工具装进一个 Java 项目,并分清「哪些逻辑该用 Mock 测、哪些必须上真容器」。
第一步:装依赖,先把版本管住
Maven 项目直接改 pom.xml。Testcontainers 有十几个模块,务必用 BOM 统一版本,否则 mysql 模块和核心模块版本错开会报 NoSuchMethodError:
<properties>
<testcontainers.version>1.20.4</testcontainers.version>
<mockito.version>5.14.2</mockito.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version><scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version><scope>test</scope>
</dependency>
<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>
</dependencies>
Spring Boot 项目更省事:spring-boot-starter-test 已经带了 JUnit 5 和 Mockito,只补 Testcontainers 那两条即可。
第二步:确认 Surefire 版本(新手最常翻车的地方)
跑 mvn test,如果输出 Tests run: 0 却 BUILD SUCCESS,八成是 Surefire 太老,不认 JUnit 5 的引擎。显式锁定版本:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
集成测试建议用 Failsafe 分开跑:*Test.java 归 Surefire(mvn test),*IT.java 归 Failsafe(mvn verify),这样日常开发不用每次都启 Docker。
第三步:写 JUnit 5 测试,先把纯逻辑测干净
JUnit 5 的入口是 org.junit.jupiter.api,别 import 成 JUnit 4 的 org.junit.Test,那个注解在 JUnit 5 下不生效:
@DisplayName("订单金额校验")
class OrderTest {
@ParameterizedTest(name = "金额 {0} 应被拒绝")
@ValueSource(ints = {0, -1, -100})
void shouldRejectNonPositive(int amount) {
assertThrows(IllegalArgumentException.class, () -> new Order(amount));
}
@Nested
@DisplayName("已支付订单")
class Paid {
@Test
void cannotRefundTwice() { /* ... */ }
}
}
@ParameterizedTest + @Nested 能把边界值和场景分组写得非常紧凑,比一堆 test1、test2 可读得多。
第四步:Mockito 隔离外部依赖
单元测试的原则是「不碰网络、不碰数据库」,那些东西全部用 @Mock 顶掉:
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock PaymentGateway gateway;
@Mock OrderRepository repo;
@InjectMocks OrderService service;
@Test
void 支付成功应落库为已支付() {
when(gateway.charge(any())).thenReturn(PaymentResult.ok("T123"));
when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));
Order order = service.pay(new OrderRequest("u1", 100));
assertEquals(OrderStatus.PAID, order.getStatus());
verify(gateway, times(1)).charge(any());
verify(repo).save(order);
}
}
注意:MockitoExtension 默认是 STRICT_STUBS 模式,stub 了但没被调用会直接抛 UnnecessaryStubbingException。这不是 bug,是在帮你清理废代码——真需要放宽时用 @MockitoSettings(strictness = Strictness.LENIENT),别一上来就全局关掉。
注意:@InjectMocks 会优先用构造器注入,其次是 setter,最后才反射写字段。如果类里既有构造器又有多参 setter,注入结果可能和你预期不一致,排查时先看构造器签名。
校验复杂参数用 ArgumentCaptor:
ArgumentCaptor<PaymentRequest> captor = ArgumentCaptor.forClass(PaymentRequest.class);
verify(gateway).charge(captor.capture());
assertEquals(100, captor.getValue().getAmount());
第五步:Testcontainers 起真数据库
涉及 SQL 方言、事务、唯一索引、JSON 字段的代码,Mock 是测不出问题的,得用真 MySQL:
@Testcontainers
@SpringBootTest
class OrderRepositoryIT {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("demo")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
}
@Container 必须写在 static 字段上,容器才会在整个测试类里只启动一次;写成实例字段会导致每个测试方法重启一次容器,一个类跑两分钟。
多个测试类共用时,抽一个基类做单例容器:
public abstract class AbstractIT {
static final MySQLContainer<?> MYSQL =
new MySQLContainer<>("mysql:8.0").withReuse(true);
static { MYSQL.start(); }
}
注意:withReuse(true) 还要在 ~/.testcontainers.properties 里加一行 testcontainers.reuse.enable=true 才生效。CI 环境别开 reuse,否则容器状态会跨构建污染。
第六步:按层分配测试类型
经验做法就三条:
- 纯计算、分支判断、异常路径 → JUnit 5 + Mockito,毫秒级,每次提交都跑。
- 涉及 SQL、事务、序列化 → Testcontainers,
mvn verify 阶段跑。
- 涉及 Controller 到 DB 的全链路 →
@SpringBootTest + Testcontainers,数量控制在个位数。
注意:Testcontainers 依赖本机 Docker。Linux 上当前用户必须在 docker 组里(sudo usermod -aG docker $USER 后重新登录),Windows/macOS 装 Docker Desktop 即可。容器结束由 Ryuk 容器负责清理,如果公司防火墙拦了 Docker socket,会出现「构建结束了容器还在跑」的现象。