You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让ArchUnit测试可复用?多Maven模块适配方案问询

复用ArchUnit测试确保各Maven模块生产类对应测试类的解决方案

核心思路

要解决单模块测试无法复用的问题,关键是把测试逻辑从硬编码的@Test方法中抽离,封装成可共享的ArchRule,同时让规则能适配不同Maven模块的包结构,动态加载当前模块的生产类和测试类。

解决方案实现

1. 封装通用的ArchRule工具类

创建一个可被所有模块依赖的测试工具类(建议放在独立的测试工具模块),把核心检查逻辑封装成通用方法:

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;

import java.util.Set;
import java.util.stream.Collectors;

public class ClassTestCoverageRules {

    // 通用方法:根据根包生成检查规则
    public static ArchRule eachProductionClassHasCorrespondingTestClass(String rootPackage) {
        // 加载当前模块的生产类(排除测试类、JAR包内容)
        JavaClasses productionClasses = new ClassFileImporter()
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
                .importPackages(rootPackage)
                .that(clazz -> !clazz.isEnum())
                .that(clazz -> clazz.isInterface() && clazz.getMethods().stream().anyMatch(JavaMethod::isDefault)); // 匹配原代码的接口筛选逻辑

        // 加载当前模块的测试类名称集合
        Set<String> testClassNames = new ClassFileImporter()
                .withImportOption(ImportOption.Predefined.ONLY_INCLUDE_TESTS)
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
                .importPackages(rootPackage)
                .stream()
                .map(JavaClass::getSimpleName)
                .collect(Collectors.toSet());

        // 定义检查条件:生产类必须有对应Test结尾的测试类
        ArchCondition<JavaClass> hasMatchingTestClass = new ArchCondition<>("have a corresponding test class") {
            @Override
            public void check(JavaClass clazz, ConditionEvents events) {
                String expectedTestName = clazz.getSimpleName() + "Test";
                if (!testClassNames.contains(expectedTestName)) {
                    events.add(SimpleConditionEvent.violated(clazz,
                            String.format("未找到对应测试类: %s", clazz.getFullName())));
                }
            }
        };

        return ArchRuleDefinition.classes().should(hasMatchingTestClass).check(productionClasses);
    }

    // 简化调用:通过锚点类自动推导根包(无需硬编码包名)
    public static ArchRule eachProductionClassHasCorrespondingTestClass(Class<?> anchorClass) {
        String rootPackage = getParentPackage(anchorClass);
        return eachProductionClassHasCorrespondingTestClass(rootPackage);
    }

    // 工具方法:获取锚点类的父包(和原代码包名处理逻辑一致)
    private static String getParentPackage(Class<?> clazz) {
        String fullPackage = clazz.getPackageName();
        return fullPackage.substring(0, fullPackage.lastIndexOf('.'));
    }
}

2. 在各Maven模块中快速复用

每个模块只需创建一个简单的测试类,用@ArchTest引用通用规则即可:

import com.tngtech.archunit.lang.ArchTest;

public class ModuleTestCoverageCheck {

    // 方式1:自动推导当前模块根包(推荐,无需硬编码)
    @ArchTest
    public static final ArchRule PRODUCTION_CLASSES_REQUIRE_TESTS =
            ClassTestCoverageRules.eachProductionClassHasCorrespondingTestClass(ModuleTestCoverageCheck.class);

    // 方式2:手动指定根包(适用于包结构特殊的模块)
    // @ArchTest
    // public static final ArchRule PRODUCTION_CLASSES_REQUIRE_TESTS =
    //         ClassTestCoverageRules.eachProductionClassHasCorrespondingTestClass("com.yourcompany.yourmodule");
}

3. 关键优化说明

  • 包名处理简化:用字符串截取替代原代码的数组拆分逻辑,更简洁高效
  • 内置逻辑替代自定义类:用Lambda表达式替代IsNotAnEnum、IsInterfaceWithDefaultMethods等自定义条件类,减少冗余代码
  • 类路径隔离:通过DO_NOT_INCLUDE_JARS确保只加载当前模块的类,避免混入依赖包内容
  • 规则复用性:核心逻辑抽离后,任何模块只需依赖测试工具类即可启用检查,无需重复编写逻辑

注意事项

  • 确保测试工具模块被其他业务模块的test scope依赖,Maven依赖配置示例:
<dependency>
    <groupId>com.yourcompany</groupId>
    <artifactId>test-utils</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>
  • 若测试类命名规则不是{类名}Test,可修改expectedTestName的生成逻辑(比如改成{类名}Tests)
  • 若需要调整生产类的筛选条件(比如排除抽象类),可修改productionClasses中的that断言逻辑

内容的提问来源于stack exchange,提问作者Oliver

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 12:54:53