- 测试
- 开发工具
【免费下载链接】junit4
A programmer-oriented testing framework for Java — :warning: maintenance mode
本文基于 doc/ReleaseNotes4.9.md 中 JUnit 4.9 的官方发布说明展开,围绕该版本的发布主题「Test-class and suite level Rules」,讲清楚两项核心 API 变更——@ClassRule注解与TestRule接口的引入——的设计动机、使用方式、源码级执行链路和校验规则,并完整继承原文档中 Maven 发布、许可证入库与全部 Bug 修复清单,帮助你在 JUnit 4 中为整个测试类或测试套件编写一次性生效的 setup/teardown 规则,并理解规则机制从方法级到类级的完整演进。
1. 版本概览:4.9 的发布主题
4.9 是 JUnit 4 系列中规则(Rules)体系的一次里程碑式升级。在此之前,@Rule只能注解实例字段,规则围绕单个测试方法生效;4.9 的主题正是「测试类级别与套件级别的规则」:
- 新增
@ClassRule注解,让静态字段(或静态方法)中声明的规则可以在整个测试类、乃至整个Suite开始前执行一次、结束后再执行一次; - 引入统一的
TestRule接口,取代只能用于方法级的旧接口MethodRule,使同一套规则机制既能作用于方法、也能作用于类; - 同时完成 Maven 构件发布流程的自主化、将许可证文件(Common Public License)检入源码仓库,并修复了一批用户反馈的 Bug(详见第 6 节)。
发布说明原文(doc/ReleaseNotes4.9.md)对该版本主题的概括是:
Release theme: Test-class and suite level Rules.
也就是说,4.9 之后,@BeforeClass/@AfterClass这类「每个类只能有一对静态生命周期方法」的限制被规则机制补上了:规则可以被组合、被继承、被排序,并且对任何继承自ParentRunner的执行器(包括标准的BlockJUnit4ClassRunner与Suite)都生效。
2. ClassRule:作用于整个测试类的规则
2.1 官方示例:套件级的一次性连接管理
发布说明给出的核心场景是:一个测试套件在运行所有测试类之前连接服务器一次,全部结束后断开。这个例子同时出现在发布说明与 ClassRule.java 的 Javadoc 中,是理解@ClassRule语义的最佳样例:
@RunWith(Suite.class) @SuiteClasses({A.class, B.class, C.class}) public class UsesExternalResource { public static Server myServer = new Server(); @ClassRule public static ExternalResource resource = new ExternalResource() { @Override protected void before() throws Throwable { myServer.connect(); } @Override protected void after() { myServer.disconnect(); } }; }ExternalResource是 JUnit 内置的资源型规则基类,见 ExternalResource.java。它的apply(Statement, Description)返回一个匿名Statement,执行顺序为:先调用before()完成资源初始化,再执行被包裹的base.evaluate(),最后无论成功失败都在finally语义中调用after()做拆除;如果before/base/after中抛出的多个异常并存,则通过MultipleFailureException.assertEmpty(errors)聚合成一次失败抛出(见 ExternalResource.java)。因此把它作为@ClassRule使用,就保证了「无论中间多少测试类、多少测试方法,资源只连接一次、必断开一次」。
@ClassRule除了注解字段,也可以注解静态方法(方法需public static且返回TestRule子类),ClassRule.java 的 Javadoc 中给出了等价的「方法形式」示例,便于在字段需要依赖注入等场景下使用。
2.2 源码链路:规则在 ParentRunner 中如何生效
从源码结构看,@ClassRule的收集与应用逻辑集中在ParentRunner中。ParentRunner.java 的classBlock(RunNotifier)构造了运行整个测试类的Statement,其组装顺序是:
protected Statement classBlock(final RunNotifier notifier) { Statement statement = childrenInvoker(notifier); if (!areAllChildrenIgnored()) { statement = withBeforeClasses(statement); statement = withAfterClasses(statement); statement = withClassRules(statement); statement = withInterruptIsolation(statement); } return statement; }可以读出两个关键事实:
- 作用范围:
@ClassRule包裹在@BeforeClass/@AfterClass之外层。结合 ClassRule.java 的 Javadoc 表述——传给规则的Statement会依次执行@BeforeClass方法、整个类体(对普通测试类是全部测试方法,对Suite是全部子测试类)、再执行@AfterClass方法。因此@ClassRule的before()一定先于@BeforeClass执行,after()一定后于@AfterClass执行,这正适合做「比测试类更早启动、更晚关闭」的基础设施管理。 - 适用执行器:由于逻辑位于抽象基类
ParentRunner,任何继承它的 Runner 都自动获得@ClassRule支持,包括BlockJUnit4ClassRunner与Suite,这与发布说明中「Any subclass ofParentRunner… will supportClassRules」的承诺一致。
规则的实际应用由 ParentRunner.java 的withClassRules与classRules()完成:先从测试类(含继承链上的注解方法/字段)收集所有@ClassRule标注、类型为TestRule的成员,再交给 RunRules 这个 4.9 新增的语句类逐一包裹。RunRules的实现非常简洁,就是对每个规则执行result = each.apply(result, description),形成「洋葱式」嵌套:
private static Statement applyAll(Statement result, Iterable<TestRule> rules, Description description) { for (TestRule each : rules) { result = each.apply(result, description); } return result; }2.3 声明校验:什么样的成员能当 ClassRule
并非任意静态字段都能被标注@ClassRule。ParentRunner.java 在collectInitializationErrors中调用了RuleMemberValidator的两个校验器,具体策略见 RuleMemberValidator.java:
CLASS_RULE_VALIDATOR(字段):声明类必须 public、字段必须 static、必须 public、字段类型必须是TestRule的子类型;CLASS_RULE_METHOD_VALIDATOR(方法):声明类必须 public、方法必须 static、必须 public、方法返回类型必须是TestRule的子类型。
违反这些约束时,JUnit 会把ValidationError汇总为初始化错误,导致该测试类无法运行——这一点可以从RuleMemberValidator的各RuleValidator内部类(MemberMustBeStatic、MemberMustBePublic、FieldMustBeATestRule等)逐条得到印证。值得注意的是,@ClassRule只认TestRule,只实现旧MethodRule接口的类型不能直接作为类级规则,这正是TestRule统一类型带来的收益。
2.4 应用顺序与 order 属性
发布说明对应版本中,若一个类上存在多个@ClassRule,其应用顺序取决于 JVM 反射 API 的实现、通常是不确定的;当前 ClassRule.java 的 Javadoc 保留了这个说明:「字段定义的规则总是晚于方法定义的规则应用(即字段规则包裹在方法规则外层)」。
需要补充的版本演进事实是:@ClassRule与@Rule注解后来增加了order()属性(Javadoc 标注@since 4.13),用于显式控制规则嵌套顺序——值大的规则在内层,默认值Rule.DEFAULT_ORDER为-1。当前 Rule.java 与 ClassRule.java 中可见该属性定义,排序实现位于 RuleContainer.java 的ENTRY_COMPARATOR。如果你维护的是 4.9 时代的代码,遇到多规则顺序问题时无法依赖order(),只能依赖「方法规则先应用」的固定约定。
2.5 用测试用例验证「一次生效」
仓库中的 ClassRulesTest.java 是验证@ClassRule行为的第一手证据。其核心用例通过计数器断言规则体只在整类级别执行一次:
public static class ExampleTestWithClassRule { @ClassRule public static Counter counter = new Counter(); // before() 中 count++ @Test public void firstTest() { assertEquals(1, counter.count); } @Test public void secondTest() { assertEquals(1, counter.count); } } @Test public void ruleIsAppliedOnce() { ExampleTestWithClassRule.counter.count = 0; JUnitCore.runClasses(ExampleTestWithClassRule.class); assertEquals(1, ExampleTestWithClassRule.counter.count); // 整类只执行一次 }同类测试还覆盖了「规则定义在父类、子类继承时同样生效」(ruleIsIntroducedAndEvaluatedOnSubclass)以及自定义TestRule(而非继承ExternalResource)作为类级规则的场景,与发布说明中「static fields that can affect the operation of a whole class」的描述互相印证。
3. TestRule:方法级与类级规则的统一类型
3.1 类型演进:MethodRule 保留但弃用
发布说明对TestRule的表述是:4.9 起,能被@Rule或@ClassRule注解的字段应当是TestRule类型;旧的MethodRule类型「仍然有效,但已弃用(will still work, but is deprecated)」。对照当前源码:
- TestRule.java:接口只声明
Statement apply(Statement base, Description description),@since 4.9; - MethodRule.java:Javadoc 明确写道「Note that
MethodRulehas been replaced byTestRule, which has the added benefit of supporting class rules」。
两个接口的差异在于apply的签名:MethodRule.apply额外接收FrameworkMethod与目标实例Object target,因此天然只面向方法级;TestRule的签名与「方法/类/套件」无关,这才使得同一个接口可以同时服务@Rule和@ClassRule。
方法级规则的应用入口在 BlockJUnit4ClassRunner.java 的withRules:它构建RuleContainer,把@Rule收集到的规则逐一注册;若某个成员同时是MethodRule与TestRule,容器会去重,避免同一条规则被双重包裹。RuleContainer内部通过apply(...)按排序结果依次调用TestRule.apply或MethodRule.apply,实现兼容两种旧类型的平滑过渡(见 RuleContainer.java)。
3.2 内置规则整体迁移到 TestRule
发布说明指出:「大多数内置规则已经迁移到了新类型上,且对大多数用户是透明的;TestWatchman被弃用,由功能相同但实现新类型的TestWatcher取代。」仓库源码可以逐条确认:
- TestWatchman.java 带有
@Deprecated注解,Javadoc 标注「UseTestWatcher(which implementsTestRule) instead」,其apply仍签名于旧的MethodRule; - TestWatcher.java(
@since 4.9)实现了TestRule,提供starting/succeeded/failed/skipped/finished五个可覆写钩子,语义与TestWatchman对应但类型统一为TestRule,因此既能作为@Rule也能作为@ClassRule使用。
TestRule的 Javadoc(TestRule.java)列出了 4.9 时点起随接口文档一起推荐的内置规则清单,可作为选型参考:ErrorCollector(单方法内聚合多个错误)、ExpectedException(灵活的异常断言)、ExternalResource(例如启停服务器)、TemporaryFolder(新建文件并在测试后删除)、TestName(记录测试名)、TestWatcher(观察执行事件)、Timeout(超时失败)、Verifier(收尾时校验对象状态)。
3.3 类级规则的边界:不是所有 TestRule 都适合作为 ClassRule
ClassRule.java 的 Javadoc 特别强调了一条使用边界:传给类级规则的Statement永远不会抛异常(异常会在规则内部被吞并处理),因此「依赖在apply之外抛出异常来判定失败」的规则,作为@ClassRule使用时行为是未定义的——文档点名ErrorCollector、ExpectedException、Timeout三种规则在此场景下不适用。这与@Rule场景形成对照:方法级规则的Statement在@Before/@Test/@After任一环失败时就会抛出异常,规则据此判定成败。写类级规则时应优先选择「包裹型」规则(如ExternalResource、TestWatcher这类在evaluate内部自行处理异常聚合并重新抛出的实现)。
4. Maven 支持:发布流程转入 JUnit 团队自身
发布说明中「Maven support」一节记录了发布流程的变化:此前 JUnit 的 Maven 构件一直由「热心的志愿者」上传;从 4.9 起,JUnit 团队开始自行完成构件的构建与发布。对使用者而言的实际影响是版本获取渠道的稳定性:从此 JUnit 4.9 及之后版本可以在 Maven 中央仓库按正式坐标直接依赖,而不再受第三方上传节奏影响。
需要说明适用前提:当前仓库的构建体系已经历后续演进——仓库根目录存在 pom.xml 与 mvnw/mvnw.cmd,构建文档见 BUILDING,即后续版本全面转向 Maven 构建;4.9 时代的 Ant 脚本(后文的build.xml)在现行仓库中已不可见,属于版本演进留下的历史痕迹。
5. LICENSE 检入仓库
发布说明指出,JUnit 所采用的许可证文本从 4.9 起正式包含在源码仓库中。当前仓库根目录可见 epl-v10.html(Eclipse Public License 文本)以及 LICENSE-junit.txt、NOTICE.txt 等许可文件,是这一变更在当前代码库中的落地形态,也便于使用者在集成、再分发时随库核查许可条款。
6. Bug 修复清单
发布说明列出的全部修复条目如下,均保留原 issue 编号,便于对照追踪:
| Issue | 问题 | 说明 |
|---|---|---|
| github#98 | assumeTrue()与 expected exception 不兼容 | 假设条件(Assumption)与expected异常机制联用时行为不正确,此版本修复 |
| github#74 | Categories + Parameterized | 4.8.2 中 Categories 执行器对内部结构「与内置 Runner 显著不同」的自定义 Runner 测试类会运行失败。修复后,此类测试类可以在类级别分配一个或多个 Category 并被正确执行;而尝试对这类类内部的方法级分配 Category 会报错 |
| github#38 | ParentRunner 重复过滤 | 致谢贡献者@reinholdfuereder。修复ParentRunner对子节点多次过滤的问题 |
| github#248 | BlockJUnit4ClassRunner#rules方法被误删 | 4.8.2 中该protected方法意外移除,4.9 恢复(方法级规则收集入口即依赖它,见 BlockJUnit4ClassRunner.java) |
| github#187 | 意外的 Java 6 依赖 | 修复构建产物对 Java 6 的意外依赖,保证运行环境要求 |
| github#163 | assertEquals(String, String)比较失败信息质量差 | 致谢@kcooney。该修复改善了字符串比较失败时的差异提示,对应实现可见 ComparisonCompactor 与 ComparisonFailure 体系 |
| github#227 | ParentRunner 假定getChildren()返回可修改列表 | 致谢@kcooney。修复ParentRunner对子类返回的getChildren()列表可变性的隐含假定 |
其中 github#74 的修复值得展开一句:Categories 过滤是通过Filter作用于ParentRunner的子节点集合实现的,当被测类使用自定义 Runner 且其内部结构(不再是「方法级叶子」)与内置 Runner 差异较大时,方法级分类过滤无处可挂。修复策略是允许类级分类,并对方法级分类请求显式报错,避免静默执行错误。
7. 次要变更
发布说明「Minor changes」一节包含三项:
- 移除无用目录,引入脚本构建测试:删除了未使用的目录
experimental-use-of-antunit,改用 bash 脚本build_tests.sh驱动测试。需要说明的是,该脚本属于 4.9 时期的构建辅助物,当前仓库已转向 Maven 构建(pom.xml),build_tests.sh在现行树中已不存在; - 多处 Javadoc 修订;
- 致谢
@kcooney的三项改进:- 将
MultipleFailureException提升为 public 类型,方便扩展开发者聚合多重失败——当前源码中该类型已位于 MultipleFailureException.java,并被TestWatcher、ExternalResource等规则实现内部复用(例如 TestWatcher.java 的MultipleFailureException.assertEmpty(errors)); - github#240:在
build.xml中新增test目标,加快 Ant 驱动的测试执行(build.xml为 4.9 时代的 Ant 构建文件,现行仓库已不再包含); - github#247:让
InitializationError携带更有用的错误消息,改善测试类初始化失败时的诊断体验,对应实现见 InitializationError.java。
- 将
8. 小结:4.9 规则体系的使用心法
回到发布说明的主题,4.9 给 JUnit 4 规则体系带来的核心能力可以归纳为三条实践结论:
- 需要「整类/整套件只做一次」的 setup/teardown 时,用
@ClassRule+TestRule(最直接的基类是ExternalResource),它包裹在@BeforeClass/@AfterClass之外,对任意ParentRunner子类执行器生效,且随继承链传递给子类(有 ClassRulesTest 用例背书); - 新代码一律面向
TestRule类型,不要再依赖MethodRule与TestWatchman(二者均处于弃用状态),这样同一条规则可无缝在@Rule与@ClassRule之间复用; - 注意适用边界与版本:
ErrorCollector/ExpectedException/Timeout不适合类级使用;多个类级规则在 4.9 中的顺序由反射决定、不可控,显式顺序控制(order())是后续版本(4.13,见 RuleContainer.java 实现)才补齐的能力;若你的工程需要 4.9 引入的 Maven 中央构件,请注意当前仓库构建体系已迁移到 Maven,历史 Ant/脚本构建物仅作版本考证参考。
- 测试
- 开发工具
【免费下载链接】junit4
A programmer-oriented testing framework for Java — :warning: maintenance mode
相关推荐
Backstage v1.7.0 发布要点深度解读:Catalog 导入后端化、权限规则 Zod Schema 化与测试栈升级
Backstage v1.7.0 发布要点深度解读:Catalog 导入后端化、权限规则 Zod Schema 化与测试栈升级 v1.7.0 是 Backsta
开发者门户后端前端如何使用RubyInstaller2在Windows上搭建Ruby开发环境?3步快速上手
如何使用RubyInstaller2在Windows上搭建Ruby开发环境?3步快速上手 RubyInstaller2是基于MSYS2的Windows Ruby
eslint-plugin-unicorn 快照测试深度解析:prefer-dom-node-text-content 规则如何把 `.innerText` 改为 `.textContent`
eslint plugin unicorn 快照测试深度解析:prefer dom node text content 规则如何把 .innerText 改为
Lint代码质量
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考