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最聪明的设计决策。

核心变化一句话: 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 的平台上,两边互不冲突。迁移的时候完全可以分步走,不用一次性翻完所有老代码。

JUnit5 架构组成 JUnit 5 JUnit Platform JUnit Jupiter JUnit Vintage IDE / 构建工具 📌 组件职责速览 • Platform — 测试引擎的基础设施,提供统一的发现与执行 API • Jupiter — 实现 JUnit5 新注解与编程模型,日常写测试的入口 • Vintage — 兼容 JUnit3/4 的旧测试,让迁移可以平滑分步进行
小提示: 在 Maven 项目中,引入 JUnit5 只需要加一个 junit-jupiter 依赖,它会自动把 Platform 和 Jupiter 都带进来。Vintage 按需添加就好,不要无脑全加。

1.3 JUnit4 vs JUnit5:核心差异对比

从JUnit4切到JUnit5,最直观的感受是注解变了。命名更清晰了,语义也更准确。下面这张表列出最常碰到的差异。

功能点JUnit4JUnit5
测试方法@Test@Test
前置方法@Before@BeforeEach
后置方法@After@AfterEach
类级前置@BeforeClass@BeforeAll(需static)
类级后置@AfterClass@AfterAll(需static)
忽略测试@Ignore@Disabled
显示名称不支持@DisplayName(支持中文)
参数化测试@Parameters(需第三方)@ParameterizedTest(原生)
异常断言@Test(expected=...)assertThrows()
多断言需手动写多个 assertassertAll()(分组断言)
扩展机制Runner + RuleExtension(接口更轻量)

我个人习惯用 @DisplayName,给测试方法起个中文描述。你想想看,测试报告一生成,「测试登录成功」、「测试密码为空时返回错误码」一目了然,产品经理看了都点赞。

再看看代码层面的对比,感受更直接:

// JUnit4 风格
@Before
public void setUp() {
    // 初始化
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidInput() {
    // ...
}

// JUnit5 风格
@BeforeEach
void setUp() {
    // 初始化
}
@Test
void testInvalidInput() {
    assertThrows(IllegalArgumentException.class, () -> {
        // ...
    });
}
踩坑提醒: 我曾经踩过一个坑——在JUnit5中写 @BeforeAll 时忘了加 static 关键字,测试一直报错。JUnit5 的 @BeforeAll 默认要求方法是静态的,除非你的测试类上加上了 @TestInstance(Lifecycle.PER_CLASS)。迁移的时候多留个心眼。

另一个让我觉得JUnit5更顺手的地方是 assertAll()。以前在JUnit4里写多个断言,第一个失败后面的就全跳过了。用了 assertAll,所有断言都会执行完,然后把失败的统一列出来,排查问题快多了。

1.4 JUnit5 的核心优势

说了这么多,JUnit5到底好在哪?我总结了几点:

  • 模块化架构 — 各组件职责清晰,按需引入,不污染依赖
  • 原生支持 Java8+ — Lambda、Stream 在测试里随便用,代码更简洁
  • 强大的参数化测试 — @ValueSource、@CsvSource、@MethodSource……各种数据源开箱即用
  • 灵活的扩展机制 — Extension 取代了笨重的 Runner 和 Rule,实现一个接口就能自定义行为
  • 更好的调试体验 — @DisplayName、assertAll、assertThrows 让测试失败信息更友好
  • 平滑的迁移路径 — Vintage 组件让你可以逐步升级,不用一刀切
我的建议: 如果你正在写新项目,直接上 JUnit5。如果有老项目在用 JUnit4,也别急着全量迁移。引入 Vintage,新测试用 Jupiter,老测试保留原样。等时机成熟了再逐步替换,风险小得多。

我刚开始用JUnit5的时候,还有点不习惯那些新注解。但用了两周之后,就再也回不去了。说白了,JUnit5 不是 JUnit4 的小修小补,而是一次架构层面的重新设计。它让写测试这件事变得更自然,更符合现代Java开发的习惯。嗯,这就是为什么咱们这门课直接选择从JUnit5开始讲起。