本文介绍如何通过 archunit 的自定义 `archcondition` 实现“每个顶层业务类必须有同名后缀为 `test` 的对应测试类”的强制校验规则,包含可复用的代码实现与关键注意事项。
在基于 Java 的模块化项目中,保障测试覆盖率不仅依赖开发自觉,更需通过架构约束实现自动化守门。ArchUnit 提供了强大的静态分析能力,但其内置规则(如 testClassesShouldResideInTheSamePackageAsImplementation())仅校验包结构一致性,无法直接验证“每个被测类是否拥有命名匹配的测试类”。要实现这一强约束,需借助 非局部条件(non-local condition) ——即让规则在执行前预先扫描全部类,构建测试类名称索引,再逐个校验业务类是否“被覆盖”。
核心思路是:
以下是完整、可直接集成的 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 ev
ents) {
// 跳过测试类自身(避免 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));
}
};
} ✅ 关键说明与最佳实践:
最后,在 CI/CD 流程中启用此规则,即可将“无测试即不合法”真正落地为工程红线——既提升可维护性,也强化团队对测试驱动文化的共识。