1. JUnit5 简介
1.1 从JUnit4到JUnit5:为什么我们需要改变?
如果你做过几年Java开发,肯定对JUnit4不陌生。从2006年发布算起,它统治了Java单元测试领域将近十年。说实话,JUnit4在设计上已经很优秀了,但Java语言本身也在进化——Lambda、Stream、模块系统……JUnit4的架构逐渐显露出一些局限。
我记得最让我头疼的是JUnit4的扩展机制。想自定义一个Runner?你得继承类,耦合很重。想写参数化测试?要么依赖第三方库,要么自己写循环,代码又丑又难维护。2015年,JUnit Lambda项目启动,目标就是打造一个模块化、可扩展的新一代测试框架。2017年,JUnit5正式发布。
这套架构的变化是根本性的。JUnit5不再是一个 monolithic 的jar包,而是拆成了三个独立组件。每个组件各司其职,互不干扰。我个人觉得,这是JUnit5最聪明的设计决策。
1.2 JUnit5 的三大组件
JUnit5 的架构由三个独立模块组成。它们分别是:JUnit Platform、JUnit Jupiter 和 JUnit Vintage。你把它想象成一个三层结构就好理解多了。
| 组件 | 角色定位 | 核心职责 | 面向谁 |
|---|---|---|---|
| JUnit Platform | 基础底座 | 定义 TestEngine API,负责测试的发现、执行与报告 | 工具开发者(IDE、构建工具) |
| JUnit Jupiter | 新测试引擎 | 实现 JUnit5 的全新注解与编程模型 | 日常写测试的开发者 |
| JUnit Vintage | 兼容引擎 | 桥接 JUnit3/4 的测试,让老代码能在 Platform 上运行 | 有遗留测试的团队 |
JUnit Platform 是最底层。它不关心你的测试怎么写,只负责「发现测试 → 执行测试 → 收集结果」。IDE 和构建工具(如 Maven Surefire、Gradle)只需要对接 Platform 的 API,就能统一运行所有测试。
JUnit Jupiter 才是我们平常写测试直接打交道的模块。@Test、@BeforeEach、@ParameterizedTest 这些新注解都来自它。Jupiter 实现了 Platform 定义的 TestEngine 接口,所以它能被 Platform 发现和执行。
JUnit Vintage 这个组件,说白了就是「新平台上的旧引擎」。我在项目中遇到过好几次——团队里既有老项目在用 JUnit4,新模块想上 JUnit5。Vintage 组件就是干这个的,它把 JUnit4 的测试跑在 JUnit5 的平台上,两边互不冲突。迁移的时候完全可以分步走,不用一次性翻完所有老代码。
1.3 JUnit4 vs JUnit5:核心差异对比
从JUnit4切到JUnit5,最直观的感受是注解变了。命名更清晰了,语义也更准确。下面这张表列出最常碰到的差异。
| 功能点 | JUnit4 | JUnit5 |
|---|---|---|
| 测试方法 | @Test | @Test |
| 前置方法 | @Before | @BeforeEach |
| 后置方法 | @After | @AfterEach |
| 类级前置 | @BeforeClass | @BeforeAll(需static) |
| 类级后置 | @AfterClass | @AfterAll(需static) |
| 忽略测试 | @Ignore | @Disabled |
| 显示名称 | 不支持 | @DisplayName(支持中文) |
| 参数化测试 | @Parameters(需第三方) | @ParameterizedTest(原生) |
| 异常断言 | @Test(expected=...) | assertThrows() |
| 多断言 | 需手动写多个 assert | assertAll()(分组断言) |
| 扩展机制 | Runner + Rule | Extension(接口更轻量) |
我个人习惯用 @DisplayName,给测试方法起个中文描述。你想想看,测试报告一生成,「测试登录成功」、「测试密码为空时返回错误码」一目了然,产品经理看了都点赞。
再看看代码层面的对比,感受更直接:
// JUnit4 风格
@Before
public void setUp() {
// 初始化
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidInput() {
// ...
}
// JUnit5 风格
@BeforeEach
void setUp() {
// 初始化
}
@Test
void testInvalidInput() {
assertThrows(IllegalArgumentException.class, () -> {
// ...
});
}
另一个让我觉得JUnit5更顺手的地方是 assertAll()。以前在JUnit4里写多个断言,第一个失败后面的就全跳过了。用了 assertAll,所有断言都会执行完,然后把失败的统一列出来,排查问题快多了。
1.4 JUnit5 的核心优势
说了这么多,JUnit5到底好在哪?我总结了几点:
- 模块化架构 — 各组件职责清晰,按需引入,不污染依赖
- 原生支持 Java8+ — Lambda、Stream 在测试里随便用,代码更简洁
- 强大的参数化测试 — @ValueSource、@CsvSource、@MethodSource……各种数据源开箱即用
- 灵活的扩展机制 — Extension 取代了笨重的 Runner 和 Rule,实现一个接口就能自定义行为
- 更好的调试体验 — @DisplayName、assertAll、assertThrows 让测试失败信息更友好
- 平滑的迁移路径 — Vintage 组件让你可以逐步升级,不用一刀切
我刚开始用JUnit5的时候,还有点不习惯那些新注解。但用了两周之后,就再也回不去了。说白了,JUnit5 不是 JUnit4 的小修小补,而是一次架构层面的重新设计。它让写测试这件事变得更自然,更符合现代Java开发的习惯。嗯,这就是为什么咱们这门课直接选择从JUnit5开始讲起。