单元测试保障架构风格

143 阅读2分钟

之前准备治理代码架构,比较苦恼的一件事情是如何保证架构规则 ( 例如依赖规则、引用规范等 ),一直苦于没有找到一个很好的保障措施,单纯靠人力检查费时费力也容易出错。柳暗花明的是,周末在与 TW 的大佬聊天得知,有这么一个工具可以单元测试的风格检查:ArchUnit

ArchUnit is a free, simple and extensible library for checking the architecture of your Java code using any plain Java unit test framework. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing given Java bytecode, importing all classes into a Java code structure.

关于这个工具的能力,直接节选官方 User Guide 第四章 -- What to Check

===================================================================================================

4. What to Check

The following section illustrates some typical checks you could do with ArchUnit.

4.1. Package Dependency Checks

package deps no access

noClasses().that().resideInAPackage("..source..")
    .should().dependOnClassesThat().resideInAPackage("..foo..")

package deps only access

classes().that().resideInAPackage("..foo..")
    .should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..")

4.2. Class Dependency Checks

class naming deps

classes().that().haveNameMatching(".*Bar")
    .should().onlyHaveDependentClassesThat().haveSimpleName("Bar")

4.3. Class and Package Containment Checks

class package contain

classes().that().haveSimpleNameStartingWith("Foo")
    .should().resideInAPackage("com.foo")

4.4. Inheritance Checks

inheritance naming check

classes().that().implement(Connection.class)
    .should().haveSimpleNameEndingWith("Connection")

inheritance access check

classes().that().areAssignableTo(EntityManager.class)
    .should().onlyHaveDependentClassesThat().resideInAnyPackage("..persistence..")

4.5. Annotation Checks

inheritance annotation check

classes().that().areAssignableTo(EntityManager.class)
    .should().onlyHaveDependentClassesThat().areAnnotatedWith(Transactional.class)

4.6. Layer Checks

layer check

layeredArchitecture()
    .consideringAllDependencies()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")

    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")

4.7. Cycle Checks

cycle check

slices().matching("com.myapp.(*)..").should().beFreeOfCycles()

===================================================================================================

上面的示例非常形象展示 ArchUnit 的特性 -- 包依赖规范检查、类依赖规范检查、目录规范检查、继承规范检查、注解规范检查、层级规范检查、循环规范检查等等。

下面示例是商品项目中一个访问规则检查 -- 禁止用户端接口调用管理段的接口 ( 类似的是:禁止核心接口调用非核接口 ):

public void api4c_should_never_access_api4b() {
    JavaClasses jc = new ClassFileImporter().importPackages("com.tencent.mall.goods.price");
    ArchRule rl = noClasses().that().haveNameMatching(".*4c.*").should().accessClassesThat()
            .haveNameMatching(".*4b.*");
    rl.check(jc);
}

需要注意的是 ArchUnit 提供的检查粒度是类 / 包级别 ( 原理上就是分析 import 关系,并不涉及更细的 invoke 分析 ),因此往往需要在类层面做重构以区分来检查 ( 不过好在都是 Ctrl C + V,多数时候直接用 IDE 提供的重构选择就可以完成 ),但同时这也非常适合使用 ArchUnit 来辅助重构检查:

ArchRule rl =
        classes().that().haveSimpleName(BasePrice4cApi.class.getSimpleName())
                .should()
                .beAnnotatedWith(FeignClient.class)
                .andShould()
                .beAnnotatedWith(
                        new DescribedPredicate<JavaAnnotation<?>>("") {
                            @Override
                            public boolean apply(JavaAnnotation<?> input) {
                                return input.getProperties().get("contextId").toString()
                                        .equals("basePrice4cApi");
                            }
                        });

For more detail, strongly recommend to read API and example in source code ( User Guide is too simple to use ) ! (www.archunit.org/assets/Arch…)