
本文介绍如何使用 archunit 编写自定义规则,确保项目中所有顶层业务类(非接口、枚举、记录、匿名类)均存在命名规范的对应测试类(如 `userservice` → `userservicetest`),并通过 `archcondition.init()` 实现跨类依赖检查。
在架构治理实践中,保障测试覆盖率不仅是质量目标,更是可维护性的基石。ArchUnit 本身不直接提供“类必须有对应测试”的内置断言,但其高度可扩展的 ArchCondition 机制允许我们构建精准、可复用的约束规则。核心思路是:先扫描全部类,提取所有以 Test 结尾的测试类名(并剥离后缀),再逐一校验每个待检业务类是否能在该集合中找到匹配项。
以下是一个生产就绪的完整实现:
@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 = Set.of(); // 初始化为空不可变集
@Override
public void init(Collection allClasses) {
// 一次性预处理:收集所有测试类对应的被测类全限定名(如 UserServiceTest → com.example.UserService)
testedClassNames = allClasses.stream()
.map(JavaClass::getName)
.filter(name -> name.endsWith(testClassSuffix))
.map(name -> name.substring(0, name.length() - testClassSuffix.length()))
.collect(Collectors.toUnmodifiableSet());
}
@Override
public void check(JavaClass clazz, ConditionEvents events) {
// 跳过测试类自身(避免自检),只检查业务类
if (clazz.getName().endsWith(testClassSuffix)) {
return;
}
boolean hasCorrespondingTest = testedClassNames.contains(clazz.getName());
String message = String.format(
"%s %s a corresponding %s class",
clazz.getSimpleName(),
hasCorrespondingTest ? "has" : "lacks",
testClassSuffix
);
events.add(new SimpleConditionEvent(clazz, hasCorrespondingTest, message));
}
};
} ✅ 关键设计说明:
- init() 方法在规则执行前被调用一次,接收整个分析范围内的所有 JavaClass 实例,适合做全局索引构建;
- 使用 toUnmodifiableSet() 提升安全性与性能,避免意外修改;
- check() 中显式跳过测试类自身,防止误报(例如 UserServiceTest 不需要 UserServiceTestTest);
- 错误消息清晰包含类名和缺失状态,便于快速定位问题。
⚠️ 注意事项:
- 确保测试类位于 ArchUnit 扫描路径中(如 src/test/java),否则 allClasses 不会包含它们;
- 若项目采用多模块结构,请在根模块或各子模块中分别声明该规则,或统一在聚合模块中配置扫描范围;
- 对于 Kotlin 类、生成代码(Lombok、MapStruct)等特殊场景,需结合 JavaClass::isSynthetic 或正则过滤进一步增强健壮性。
通过该规则,团队可在 CI 流程中自动拦截未覆盖的类,将“测试先行”从开发习惯固化为架构契约。










