如何使用 ArchUnit 强制要求每个业务类都存在对应的测试类

本文介绍如何通过 archunit 编写自定义规则,确保所有顶层业务类(非接口、枚举、记录、匿名类)均存在命名规范的对应测试类(如 `service` → `servicetest`),并提供可直接运行的完整实现与关键注意事项。

在构建高可靠性的 Java 项目时,保障测试覆盖率不仅是质量门禁的重要一环,更是持续集成中预防回归缺陷的关键手段。ArchUnit 作为静态架构分析利器,不仅能校验分层、包依赖等宏观结构,还可通过自定义条件精准约束“类-测试类”的映射关系。下面将演示如何强制要求每个待测业务类必须拥有一个命名合规的对应测试类。

核心思路:基于全局类集合的双向匹配

ArchUnit 的 ArchCondition 默认以单个 JavaClass 为单位执行检查,但要验证“每个类是否有对应测试类”,需先遍历全部类,提取所有符合 *Test 命名模式的测试类名,并将其映射为被测类名(例如 UserServiceTest → UserService)。这需要重写 init(Collection) 方法,在规则执行前完*局预处理。

完整可运行规则示例

@ArchTest
static final ArchRule relevant_cl

asses_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)); } }; }

关键注意事项与最佳实践

  • 适用范围明确:该规则默认仅检查顶层类(areTopLevelClasses()),自动排除内部类、Lambda 生成类等干扰项;如需覆盖更多类型,可调整前置筛选条件(如增加 .and().areNotAnonymousClasses())。
  • ⚠️ 命名约定需统一:当前逻辑严格依赖 *Test 后缀(如 CalculatorTest 对应 Calculator)。若项目采用 *Tests、*IT 或 *IntegrationTest 等变体,需同步修改 testClassSuffix 参数及 init() 中的过滤逻辑。
  • ? 包结构无关性:本实现不强制测试类与被测类同包(如需此约束,可参考 GeneralCodingRules.testClassesShouldResideInTheSamePackageAsImplementation() 的源码扩展 packageName 匹配逻辑)。
  • ? 测试驱动开发友好:规则会在编译期即报错,促使开发者“先写测试再写实现”,天然契合 TDD 流程。
  • ? 依赖版本兼容性:示例基于 ArchUnit 1.0.1+,低版本请确认 ArchCondition.init() 方法可用性;建议升级至最新稳定版(如 1.3.0+)以获得更优性能与 API 支持。

通过上述规则,你不仅实现了对测试覆盖率的自动化强约束,更将质量保障左移到编码阶段——每一次 mvn test 或 IDE 运行都会实时反馈缺失的测试类,真正让“有代码必有测试”成为团队可落地的工程实践。