ArchUnit 实战:强制要求每个业务类都具备对应测试类

本文介绍如何使用 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) {
    re

turn 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 流程中自动拦截未覆盖的类,将“测试先行”从开发习惯固化为架构契约。