- 静态分析
- 代码质量
- 开发工具
【免费下载链接】error-prone
Catch common Java mistakes as compile-time errors
JUnit 3 依靠"方法名以test开头 +public void无参签名 + 继承TestCase"的约定来发现测试。只要前缀拼错、漏加test前缀、或签名不合规,测试就会被 JUnit 3 静默跳过——不报错、不失败,只在构建报告中少了几条用例。Error Prone 内置的JUnit3TestNotRun检查器专治此类问题,它能在编译期把这类"永远不会被执行"的方法标为 ERROR,并自动给出改名、提权、去static的修复建议。阅读本文,你将掌握该检查器的完整触发规则、防误报机制,以及如何利用它和它的自动修复让你的 JUnit 3 测试真正跑起来。
一、问题背景:JUnit 3 的"按命名约定发现测试"机制
JUnit 3(junit.framework.TestCase时代)没有任何注解,测试框架通过反射扫描来发现用例,约定的匹配规则非常严格:测试方法必须满足
- 方法名以
test开头(大小写敏感); public可见性;- 无参数;
- 返回
void。
这条约定在 JUnitMatchers.java 中被直接建模为isJunit3TestCasematcher:
public static final Matcher<MethodTree> isJunit3TestCase = allOf( methodNameStartsWith("test"), methodHasNoParameters(), Matchers.<MethodTree>hasModifier(Modifier.PUBLIC), methodReturns(VOID_TYPE));任何一个条件不满足,方法都会被 JUnit 3 的反射扫描忽略。最糟糕的是这种忽略是静默的:编译不失败、运行不报错,测试就这样悄悄"消失"了。这正是 docs/bugpattern/JUnit3TestNotRun.md 想要解决的问题——它把触发该错误的方法归纳为三类:
- 拼错
test前缀(如把testXxx写成tesXxx); - 有
@Test注解但没有test前缀(JUnit 3 不认注解,只认名字); - 签名错误(非
public、带参数、返回非void、是static等)。
以上任何情况都会导致 JUnit 3 忽略该方法。
二、检查器如何判定"这是一个本该执行的测试"
JUnit3TestNotRun的实现位于 core/src/main/java/com/google/errorprone/bugpatterns/JUnit3TestNotRun.java,它实现的是CompilationUnitTreeMatcher(在整个编译单元上扫描,而非单个方法),因为部分判定需要"全文件视角"(例如要知道某方法是否在别处被调用)。
2.1 逐层过滤的checkMethod判定流程
核心逻辑在checkMethod(JUnit3TestNotRun.java#L130-L154),它按顺序执行以下检查,全部通过才报告问题:
- 所在类必须是 JUnit 3 测试类:
enclosingClass(isJUnit3TestClass)。isJUnit3TestClass(见 JUnitMatchers.java#L144-L145)要求:继承自junit.framework.TestCase、没有@RunWith注解、没有任何@Test注解方法、非 abstract、且是顶层类。 - 该方法本身不是一个合规的 JUnit 3 测试:
isJunit3TestCase命中则直接放过(说明它是正常测试)。 - 方法"长得像测试"(
LOOKS_LIKE_TEST_CASE,JUnit3TestNotRun.java#L85-L89):无参数或为public、返回void、且所在类名不以Base结尾。 - 方法没有被调用过:通过预先扫描整个编译单元收集的
calledMethods集合排除——如果该方法是工具方法且被其他代码调用,说明它本来就不是测试,不应误报。 - 方法不是 override:
findSuperMethods非空则放过(例如实现接口Foo { void testDoesStuff(boolean); }的方法会被排除)。 - 名字命中可疑模式:方法名不以
test开头,但满足以下任一条件才继续:- 命中
MISSPELLED_NAME拼写错误正则; - 或
wouldRunInJUnit4(有@Test注解且无@Ignore)——即"注解了但名字不对"的情况。
- 命中
2.2 拼写错误正则:MISSPELLED_NAME
源码中精心设计了一个正则来捕获test的各种常见拼写错误(JUnit3TestNotRun.java#L72-L83):
private static final Pattern MISSPELLED_NAME = Pattern.compile( "t.est|te.st|" + // 多插了一个字母 "tst|tet|tes|" + // 少了一个字母 "etst|tset|tets|" + // 字母顺序颠倒 "t.st|te.t|" + // 换了一个字母 "[tT][eE][sS][tT]"); // 大小写混乱注释中还专门解释了为什么不匹配.est、est、.test:
restore、destroy、best、establish等真实单词会因此被排除,避免误报;- 有人刻意用
.test(如disableThisTest)来禁用测试,test后面的内容属于合法命名,因此.test也不在正则内。
测试用例 JUnit3TestNotRunTest.java#L355-L416 中的negativeCase1验证了这一点:bestNameEver()、destroy()、restore()、establish()、estimate()这些真实单词都不会被报告。
三、触发示例:哪些方法会被报告
从测试文件 JUnit3TestNotRunTest.java 的positiveCases与misspelledTest可以归纳出典型触发样例:
import junit.framework.TestCase; public class ExampleTest extends TestCase { // 拼写错误:tes 开头(缺字母) public void tesName() {} // 拼写错误:tets 开头(字母颠倒) public void tetsName() {} // 拼写错误:teat 开头(换字母) public void teatName() {} // 大小写混乱:Test 开头 public void TestName() {} // 有 @Test 注解但名字没有 test 前缀 → 仍会被 JUnit 3 忽略 @Test public void doesStuff() {} // 签名错误:private(JUnit 3 需要 public) private void testDoesStuff() {} // 签名错误:static(JUnit 3 需要实例方法) public static void testParseSomething() {} // 签名错误:带参数(JUnit 3 测试方法必须无参) public void testDoesStuff(boolean param) {} }对照修复后的正确形态:public void testName()、public void testDoesStuff()。注意测试privateNamedTest、privateMisspelledTest、hasParameters_butOtherwiseLooksLikeATestMethod均在测试中被标注了BUG: Diagnostic contains:,证明这些情况都会被报告。
四、防误报设计:哪些情况会被刻意放过
这个检查器在准确性上做了大量收敛,以下情况不会被报告(均有测试佐证):
| 场景 | 理由 | 测试用例 |
|---|---|---|
方法名正确拼写test前缀 | 是正常测试 | negativeCase1 |
名字以真实单词开头(best/destroy/restore等) | 正则刻意排除 | negativeCase1 |
非void返回类型 / 带参数的方法 | 不像测试 | negativeCase1 |
所在类是 JUnit 4 类(@RunWith(JUnit4.class)) | JUnit 4 靠注解发现测试 | negativeCase2 |
混合 JUnit3+4,类上有@RunWith | 无法确定运行方式 | negativeCase3、negativeCase5 |
abstract测试类 | 无法确定运行方式 | negativeCase4 |
| 方法在类内被其他方法调用 | 是工具方法而非测试 | hasParameters_calledElsewhere_noFinding |
| 方法是 override | 不归 JUnit 3 负责 | hasParameters_isOverride_noFinding |
基类(类名以Base结尾)中的方法 | 可能是模板方法 | hasParameters_butInABaseClass |
setUp()生命周期方法 | JUnit 3 特殊处理 | setupMethod_shouldBeIgnored |
这种"宁可放过、不可误报"的取向也体现在源码注释中(JUnit3TestNotRun.java#L63-L71):作者明确表示有意排除了.est与est等模式,因为会命中真实单词;对于tets → tests这类纠正结果不够优雅的拼写,也选择"宁要简单正则、保留误报风险可控"的方案。
五、修复方案:自动修复与手动重命名
5.1 Error Prone 的自动修复(describeFixes)
当方法被命中时,检查器会构建一条SuggestedFix(JUnit3TestNotRun.java#L156-L173),自动执行三步修复:
- 重命名:若方法名不以
test开头——- 命中
MISSPELLED_NAME正则的,把拼错的部分直接替换为test(如tesName→testName); - 否则把方法名改造为
test+ 首字母大写的驼峰形式(如doesStuff→testDoesStuff)。
- 命中
- 提升可见性:通过
SuggestedFixes.Visibility.PUBLIC.refactor把方法改为public(private void testDoesStuff()→public void testDoesStuff())。 - 移除
static修饰符:removeModifiers(..., Modifier.STATIC)。
这段修复逻辑被BugCheckerRefactoringTestHelper的测试完整验证过。例如misspelledTest测试(JUnit3TestNotRunTest.java#L75-L136)展示了tesName1、ttestName2、teestName3、tstName4、etstName6、TEST_NAME_10、tesname11等分别被自动纠正为testName1、testName2、testName3、testName4、testName6、test_NAME_10、testname11;hasModifiersAndThrows测试则验证了private static void tsetDoesStuff() throws Exception被一键修复为public void testDoesStuff() throws Exception(throws子句保留)。
5.2 手动修复:遵循原文档建议
原文档 docs/bugpattern/JUnit3TestNotRun.md 给出的手动修复建议是:
- 若本意是禁用该测试,或这是辅助方法:改成一个更有描述性的名字,例如
disabledTestSomething(); - 不需要
@Test注解;如果确实想保留注解,请同时加上@Ignore,明确表达"我有意停用"。
@Test+@Ignore的组合在negativeCase1测试中有对应的负例验证——@Test @Ignore public void ignoredTest() {}与@Ignore @Test public void ignoredTest2() {}都不会被报告。其底层依据是wouldRunInJUnit4matcher 要求"有@Test且无@Ignore"(JUnitMatchers.java#L238-L241)。
5.3 抑制(Suppress)
如果某个方法确实需要保留原样(例如带参数的testDoesStuff(boolean)且不想改名),可以在方法上添加@SuppressWarnings("JUnit3TestNotRun"),这在测试suppressionWorks(JUnit3TestNotRunTest.java#L259-L272)中已得到验证。
六、严重级别与默认启用状态
JUnit3TestNotRun的@BugPattern注解将严重级别设为ERROR(JUnit3TestNotRun.java#L56-L60),摘要为:
Test method will not be run; please correct method signature (Should be public, non-static, and method name should begin with "test").
这意味着一旦命中,默认配置下会直接阻断编译(而不是仅仅给出警告),让"测试没跑"这类问题在编译期暴露。它被注册进内置检查器集合的ENABLED_ERRORS(BuiltInCheckerSuppliers.java#L813),无需额外配置即可随 Error Prone 编译启用。若你的项目还在维护 JUnit 3 测试,强烈建议保持其默认启用状态。
七、延伸阅读:JUnit 4 时代的姊妹检查器
与JUnit3TestNotRun相对的是 JUnit4TestNotRun(源码注册于 BuiltInCheckerSuppliers.java#L817)。它解决的是 JUnit 4 中的镜像问题:JUnit 4 靠@Test注解发现测试,一个"看起来像测试"(名字以test开头、签名正确)但没有@Test注解的方法同样不会被运行。其修复建议同样是:有意停用就加@Test+@Ignore,是辅助方法就降低可见性(非public)。
这两个检查器共同覆盖了 JUnit 3 与 JUnit 4 两代测试框架的"静默跳过"场景,配合 Error Prone 编译期检查与自动修复,可以让开发者把精力从"排查测试为什么没跑"转移到真正的业务逻辑上。
总结
JUnit3TestNotRun是一个高度"克制"的编译期检查器:它以 JUnit 3 的命名约定为判定依据,用精心设计的拼写正则和层层防误报过滤(排除真实单词、被调用方法、override、JUnit 4 类、abstract 类、Base基类等),只报告真正"本该是测试却不会被运行"的方法,并以 ERROR 级别阻断编译、提供一键自动修复。如果你维护着任何继承junit.framework.TestCase的测试代码,让 Error Prone 的这条检查器保持开启,是最廉价也最可靠的"测试静默丢失"防线。
- 静态分析
- 代码质量
- 开发工具
【免费下载链接】error-prone
Catch common Java mistakes as compile-time errors
相关推荐
Error Prone MissingFail 检查器:让 JUnit 异常测试不再漏掉 fail() 调用
Error Prone MissingFail 检查器:让 JUnit 异常测试不再漏掉 fail 调用 本指南围绕 Error Prone 项目中的 Miss
静态分析代码质量开发工具Error Prone 构造器链检查 ChainingConstructorIgnoresParameter:编译期捕获被忽略的透传参数
Error Prone 构造器链检查 ChainingConstructorIgnoresParameter:编译期捕获被忽略的透传参数 在 Java 中,构造
静态分析代码质量开发工具Error Prone 常量溢出检查(ConstantOverflow):让编译期常量运算溢出无处遁形
Error Prone 常量溢出检查(ConstantOverflow):让编译期常量运算溢出无处遁形 导读 本篇文章聚焦 Error Prone 静态分析工具
静态分析代码质量开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考