ARTICLE · 1005959
源码级深挖 JUnit 6:三层架构 + 完整执行链路,17 项差距甩开 JUnit 4
junit4 仓库:8515 Star,0 个 open issue,2009 年创建 junit-framework(原 junit5 官方仓库):7043 Star,每周仍有代码提交,2015 年创建
一、仓库改名:藏着版本管理的工程智慧
Vintage引擎在 6.x 已经 deprecated,官方定位是 “迁移期的临时桥梁”,不是长期方案。 6.0删除了junit-platform-runner模块,当年从 JUnit 4 迁 5 时走@RunWith(JUnitPlatform.class)的团队,升 6.0 会直接编译失败。
二、三层架构:为什么IDEA能同时跑JUnit 4和6
junit-jupiter 引擎:执行 JUnit 5/6 测试 junit-vintage 引擎:执行 JUnit 3/4 测试(已 deprecated) 第三方引擎:Spock、Cucumber 等都遵循这套规范
模块划分的三层:Platform(平台)/ Jupiter(编程模型 + 引擎)/ Vintage(兼容引擎)。Jupiter 和 Vintage 是平级的引擎,不是上下层关系。 Platform内部的分层:Launcher → TestEngine SPI → Reporting。这是真正的分层架构。
三、发现+执行:从“静默失效”到“可观测可管控”
发现阶段:终结“静默跳过”的隐形坑
发现阶段的所有异常(方法签名不符、可见性错误、注解误用)都会被收集 支持 INFO / WARNING / ERROR 三个级别,可配置 critical 阈值,达到阈值直接阻断执行 默认值为 ERROR,官方建议先设成 INFO 摸全量问题,清理后再回调等级
执行阶段:13行代码说透回调顺序
扩展在外层,用户方法在内层:BeforeEach 扩展回调先于@BeforeEach方法执行,这也是 Mockito 能在前置方法里注入 mock 对象的底层原因。 扩展的清理逻辑一定执行:哪怕前置扩展抛出异常,AfterEach 扩展回调也一定会执行,从机制上保证资源释放。 用户@AfterEach在@BeforeEach失败时仍会执行:这是刻意设计,给用户清理半初始化资源的机会,因此@AfterEach里一定要做 null 检查。 测试执行前后回调严格配对:专门用于精准统计测试方法本身耗时,不会出现有前无后的情况。
四、6.1核心更新:新执行器与五大新能力
为什么要换并行执行器
6.1的五个新能力
Pioneer三扩展进核心:@DefaultLocale、@DefaultTimeZone、@SetSystemProperty从 JUnit Pioneer 正式收编进核心,不再需要额外依赖。 junit-start模块:配合Java 25 紧凑源文件特性,一个Java 文件、一个main 方法就能跑测试,适合演示、快速验证场景。 常量上移到 API 模块:配置常量全部移到 api 模块的Constants类,享受编译期检查,告别手写字符串写错静默失效。 @TempDir删除策略:解决 Windows 环境下临时目录删不掉导致测试失败的痛点,支持 IgnoreFailures 策略。 内存清理模式:超大测试套件的性能救星,执行完的测试节点立即移除,大幅降低内存峰值。
五、对比JUnit 4:17处本质提升
扩展体系:从“单Runner硬凑”到“全生命周期可插拔”
Runner单一限制彻底解除,扩展可无限叠加JUnit 4 中一个测试类只能标注一个@RunWith,想同时集成Spring、Mockito、参数化测试,要么用@Rule勉强拼凑,要么自己手写复合 Runner,组合成本极高。JUnit 6 中扩展是完全可插拔的平级组件,通过@ExtendWith可以注册任意多个扩展,互不冲突,组合自由度直接拉满。 扩展点从寥寥数个扩展到约 20 个专用接口JUnit 4 核心扩展点只有Runner、MethodRule/TestRule、Statement、RunListener 寥寥几个,想做细粒度控制只能靠绕路 hack。JUnit 6 提供了覆盖测试发现、实例创建、前置后置、参数注入、异常处理的全生命周期扩展接口,每个场景都有标准SPI,扩展开发的规范性和可维护性大幅提升。 构造函数和测试方法原生支持参数注入JUnit 4 的测试类必须有无参构造函数,测试方法不能携带参数,想传参只能通过@Rule维护静态变量绕弯。JUnit 6 原生支持构造函数参数与测试方法参数,通过ParameterResolver扩展可以直接注入 Spring 上下文、Mock 对象、临时资源等,不用手动管理全局状态。
执行机制:从“黑盒静默”到“可观测、可管控、可并行”
测试方法不再强制 public,写错有明确告警JUnit 4 中测试方法必须是public void无参,写成 private、protected 或者带返回值都会静默跳过,连警告都没有,漏测了很难排查。JUnit 6 不仅放宽了可见性要求(protected、包可见均可正常执行),签名不符合规范还会通过DiscoveryIssue 机制抛出明确的错误分级提示。 DiscoveryIssue机制彻底终结 “静默跳过”JUnit 4 里测试不跑的原因千奇百怪:方法签名错、注解放错位置、类非public、静态方法,全部静默忽略,排查全靠经验猜。JUnit 6 的DiscoveryIssue 会收集所有发现阶段的异常,支持三级严重度分级,达到阈值直接阻断执行,把 “为什么没跑” 明明白白列出来。 引擎级标准并行执行,统一配置 + 资源锁体系JUnit 4 没有原生并行能力,想并行要么靠Maven 插件要么自定义Runner,不同Runner 并行逻辑不统一,资源竞争全靠手动加锁。JUnit 6 原生支持类、方法、嵌套多维度并行,内置资源锁机制,通过@ResourceLock声明共享资源,框架自动保证线程安全,不用手写同步代码。 声明式条件执行,准备阶段判定跳过,不浪费资源JUnit 4 的@Ignore只能硬编码跳过,想按环境、配置、系统属性条件跳过,得自己写 Rule 判断,而且是执行到测试方法才跳,前置资源已经加载完了。JUnit 6 的@EnabledIf、@DisabledIf等条件注解在发现阶段就判定是否执行,不满足条件的测试连实例都不会创建,大幅节省初始化开销。 原生支持嵌套测试,共享前置状态JUnit 4 想写分层测试只能靠类继承,或者内部类加Runner 适配,代码冗余且结构混乱。JUnit 6 用@Nested就可以实现多层嵌套测试类,内层可以共享外层的@BeforeEach状态和字段,业务场景分层表达更自然,代码结构更清晰。
能力边界:从“基础工具”到“工业级测试平台”
方法级参数化成为一等公民,内置 10 种数据源JUnit 4 的参数化测试要给整个类加@RunWith(Parameterized.class),一个类只能有一种参数化逻辑,写法笨重且数据源单一。JUnit 6 的@ParameterizedTest是方法级的,一个类里可以同时有普通测试和多组参数化测试,内置@ValueSource、@CsvSource、@MethodSource等 10 种数据源,还支持自定义扩展。 支持运行时生成的动态测试JUnit 4 的测试用例都是编译时固定的,想根据文件、接口返回值动态生成用例,只能靠反射或者自定义Runner 实现。JUnit 6 提供@TestFactory,运行时可以动态生成测试用例,非常适合批量验证接口、数据驱动测试的场景。 断言体系原生升级:assertAll、assertThrows、assertTimeoutJUnit 4 原生断言能力很弱:多条件断言第一个失败后面就不执行,断言异常得手写try-catch,超时测试靠Rule 配置。JUnit 6 原生支持assertAll批量断言(全部执行完统一报失败)、assertThrows精准断言异常、assertTimeout超时控制,不用额外依赖第三方断言库也能写出清晰的测试。 灵活的显示名生成与参数化占位符JUnit 4 测试报告里只能看到方法名,参数化测试只能看到序号,失败了很难定位是哪组参数。JUnit 6 支持自定义测试显示名,参数化测试支持{0}、{arguments}等占位符自动替换参数值,失败日志可读性大幅提升。 执行顺序可控、可复现,支持随机种子JUnit 4 的测试执行顺序依赖反射顺序,不同JVM、不同环境结果可能不一样,隐式依赖导致的偶发失败很难复现。JUnit 6 支持配置执行顺序,随机模式可以固定种子,既能随机执行暴露隐式依赖,又能保证问题可复现。 统一的配置参数体系,四级配置源JUnit 4 没有统一配置体系,Runner、Rule 各有各的配置方式,大多只能硬编码。JUnit 6 有统一的配置体系,支持 Launcher API、配置文件、JVM 系统属性、junit-platform.properties四级配置源,优先级明确,全局配置和局部覆写都很方便。 现代化报告格式,支持 Open Test Reporting 事件流JUnit 4 只有简单的文本 / XML 报告,格式老旧,不同测试引擎输出不统一。JUnit 6 支持Open Test Reporting 标准事件流,结构化输出测试执行全流程,方便CI 平台、质量平台统一消费和可视化。 JFR飞行记录集成收进主线JUnit 4 没有原生性能监控能力,排查测试慢、死锁只能靠外部工具。JUnit 6 内置JFR 飞行记录支持,可以记录每个测试阶段的耗时、线程事件,性能瓶颈可以直接定位到具体方法和回调。 原生支持取消执行与 fail-fastJUnit 4 想中途停止测试只能强制杀进程,没有优雅取消机制,也没有统一的fail-fast 开关。JUnit 6 内置CancellationToken支持优雅取消,控制台原生支持--fail-fast参数,第一个测试失败即可立即停止后续执行,节省CI 时间。
六、上生产的8条配置建议
1. 发现问题门禁:先设 info 摸全量,清理完再回调到 errorjunit.platform.discovery.issue.severity.critical = info# 2. CI 开启超时自动线程 dump,排查偶发超时神器junit.jupiter.execution.timeout.threaddump.enabled = true# 3. 超时按类型分层,启动容器和跑用例不该一个值junit.jupiter.execution.timeout.beforeall.method.default = 5 mjunit.jupiter.execution.timeout.testable.method.default = 10 s# 4. 并行执行:mode.default 必配,否则一个测试都不会并行junit.jupiter.execution.parallel.enabled = truejunit.jupiter.execution.parallel.mode.default = concurrentjunit.jupiter.execution.parallel.mode.classes.default = concurrentjunit.jupiter.execution.parallel.config.strategy = dynamicjunit.jupiter.execution.parallel.config.dynamic.factor = 0.5# 切换 6.1 新执行器(experimental,建议预发压测后再开)junit.jupiter.execution.parallel.config.executor-service = worker_thread_pool# 5. 大套件开内存清理,混合场景排除 Vintagejunit.platform.execution.memory.cleanup.enabled = truejunit.platform.execution.memory.cleanup.engines.excluded = junit-vintage# 6. Windows CI 开启临时目录删除忽略失败junit.jupiter.tempdir.deletion.strategy.default = org.junit.jupiter.api.io.TempDirDeletionStrategy$IgnoreFailures# 7. 输出 Open Test Reporting XML,便于 CI 统一消费junit.platform.reporting.open.xml.enabled = true# 8. 随机顺序执行并固定种子,暴露隐式依赖且结果可复现junit.jupiter.execution.order.random.seed = 20260902 |