
本文介绍如何通过 archunit 的自定义 `archcondition` 实现“每个顶层业务类必须有同名后缀为 `test` 的对应测试类”的强制校验规则,包含可复用的代码实现与关键注意事项。
在基于 Java 的模块化项目中,保障测试覆盖率不仅依赖开发自觉,更需通过架构约束实现自动化守门。ArchUnit 提供了强大的静态分析能力,但其内置规则(如 testClassesShouldResideInTheSamePackageAsImplementation())仅校验包结构一致性,无法直接验证“每个被测类是否拥有命名匹配的测试类”。要实现这一强约束,需借助 非局部条件(non-local condition) ——即让规则在执行前预先扫描全部类,构建测试类名称索引,再逐个校验业务类是否“被覆盖”。
核心思路是:
- 收集所有以 "Test" 结尾的类(如 UserServiceTest),提取其对应业务类名(如 UserService);
- 对每个符合条件的业务类(非接口、非枚举、非 record、非匿名类、顶层类),检查其全限定名是否存在于上述索引中;
- 若不存在,则触发失败事件,阻断构建。
以下是完整、可直接集成的 ArchUnit 测试规则:
@ArchTest
static final ArchRule relevant_classes_should_have_tests =
classes()
.that()
.areTopLevelClasses()
.and().areNotInterfaces()
.and().areNotRecords()
.and().areNotEnums()
.should(haveACorrespondingClassEndingWith("Test"));
private static ArchCondition haveACorrespondingClassEndingWith(String testClassSuffix) {
return new ArchCondition("have a corresponding class with suffix " + testClassSuffix) {
private Set testedClassNames = Collections.emptySet();
@Override
public void init(Collection allClasses) {
this.testedClassNames = allClasses.stream()
.map(JavaClass::getName)
.filter(name -> name.endsWith(testClassSuffix))
.map(name -> name.substring(0, name.length() - testClassSuffix.length()))
.collect(Collectors.toSet());
}
@Override
public void check(JavaClass clazz, ConditionEvents events) {
// 跳过测试类自身(避免 self-match)
if (clazz.getName().endsWith(testClassSuffix)) {
return;
}
boolean hasCorrespondingTest = testedClassNames.contains(clazz.getName());
String message = String.format(
"%s %s a corresponding test class ending with '%s'",
clazz.getSimpleName(),
hasCorrespondingTest ? "has" : "lacks",
testClassSuffix
);
events.add(new SimpleConditionEvent(clazz, hasCorrespondingTest, message));
}
};
} ✅ 关键说明与最佳实践:
- ✅ init() 方法是关键:ArchUnit 会自动在规则执行前调用它,并传入当前扫描范围内所有 JavaClass 对象,因此可安全构建全局索引;
- ✅ 跳过测试类自身校验:check() 中显式 return 掉以 Test 结尾的类,防止误判(如 UserServiceTest 错误地被要求匹配 UserServiceTestTest);
- ⚠️ 注意类路径范围:该规则默认作用于 classes() 扫描的所有类(含 src/main/java 和 src/test/java)。若项目结构复杂(如多模块、测试类位于不同 source set),建议配合 importOption 精确控制扫描范围;
- ? 支持灵活后缀:将 "Test" 替换为 "IT" 或 "IntegrationTest" 即可适配集成测试命名规范;
- ? 可组合增强:可进一步结合 resideInAnyPackage(...) 限定校验范围(如仅检查 com.example.service.. 下的类),避免污染第三方或配置类。
最后,在 CI/CD 流程中启用此规则,即可将“无测试即不合法”真正落地为工程红线——既提升可维护性,也强化团队对测试驱动文化的共识。










