CLOVER🍀

That was when it all began.

Javaソースコードのアーキテクチャーをチェックするテストが書ける、ArchUnitを試す

これは、なにをしたくて書いたもの?

ArchUnitというものを試してみたいな、ということで。

ArchUnit

ArchUnitは、Javaソースコードアーキテクチャーをチェックするためのユニットテストフレームワークです。

ArchUnit is a free, simple and extensible library for checking the architecture of your Java code using any plain Java unit test framework.

Unit test your Java architecture - ArchUnit

ArchUnitを使うことで、パッケージやクラス、レイヤーやスライスの依存関係、循環参照などをチェックできます。

That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more.

Why test your architecture? - ArchUnit

Getting Startedやドキュメントはこちら。

Getting Started - ArchUnit

Use Cases - ArchUnit

ArchUnit User Guide

archunit 1.0.1 javadoc (com.tngtech.archunit)

ユースケースのページを見ると、イメージしやすいですね。

サンプルはこちら。

https://github.com/TNG/ArchUnit-Examples

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.7 2023-04-18
OpenJDK Runtime Environment (build 17.0.7+7-Ubuntu-0ubuntu122.04.2)
OpenJDK 64-Bit Server VM (build 17.0.7+7-Ubuntu-0ubuntu122.04.2, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.2 (c9616018c7a021c1c39be70fb2843d6f5f9b8a1c)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.7, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-72-generic", arch: "amd64", family: "unix"

準備

Maven依存関係など。今回は、チェックの都合上、Jakarta EEのWeb Profileを使うことにします。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.24.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

使用するテスティングフレームワークは、JUnit 5とします。

ArchUnitを依存関係に加える

ArchUnitを依存関係に加えましょう。JUnit 5向けの場合はこちらになります。

        <dependency>
            <groupId>com.tngtech.archunit</groupId>
            <artifactId>archunit-junit5</artifactId>
            <version>1.0.1</version>
            <scope>test</scope>
        </dependency>

Installation / JUnit 5

JUnit 4向けの場合はこちら。

Installation / JUnit 4

基本的な使い方

基本的な使い方は、Getting Started(User Guide含む)を見るのがよいのではないかと思います。

Getting Started - ArchUnit

Getting Started

Getting Startedページのサンプルがだいぶ物悲しいのですが、こちらが基本になります。

    @Test
    public void some_architecture_rule() {
        JavaClasses importedClasses = new ClassFileImporter().importPackages("com.myapp");
    
        ArchRule rule = classes()... // see next section
    
        rule.check(importedClasses);
    }

ステップとしては、「クラスをインポートする」、

        JavaClasses importedClasses = new ClassFileImporter().importPackages("com.myapp");

アーキテクチャーのルールを定義する」、

        ArchRule rule = classes()... // see next section

「インポートしたクラスに対して、定義したルールを使ってチェックする」となります。

        rule.check(importedClasses);

ユーザーガイドでは、こちら。

こちらは、「com.mycompany.myappパッケージ内を対象に、serviceパッケージ内のクラスはcontrollerまたは
serviceパッケージからのみアクセス可能」というチェックをしています。

@Test
public void Services_should_only_be_accessed_by_Controllers() {
    JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.myapp");

    ArchRule myRule = classes()
        .that().resideInAPackage("..service..")
        .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

    myRule.check(importedClasses);
}

このあたりが読めないと、なにをやっているのかよくわからなくなります…。

チェック対象

とりあえず、ArchUnitでのチェック対象がないと始まりません。

こんな感じのソースコードを用意しました。

$ tree src/main/java/org/littlewings/archunit
src/main/java/org/littlewings/archunit
├── JaxrsActivator.java
├── bar
│   ├── repository
│   │   └── BarRepository.java
│   ├── resource
│   │   └── BarResource.java
│   └── service
│       └── BarService.java
└── foo
    ├── repository
    │   └── FooRepository.java
    ├── resource
    │   └── FooResource.java
    └── service
        └── FooService.java

8 directories, 7 files

JAX-RSCDIの構成にしました。

JAX-RSの有効化。

src/main/java/org/littlewings/archunit/JaxrsActivator.java

package org.littlewings.archunit;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
}

JAX-RSリソースクラス。

src/main/java/org/littlewings/archunit/foo/resource/FooResource.java

package org.littlewings.archunit.foo.resource;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.littlewings.archunit.foo.service.FooService;

@Path("foo")
@ApplicationScoped
public class FooResource {
    @Inject
    FooService fooService;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String foo() {
        return fooService.message();
    }
}

Serviceクラス。

src/main/java/org/littlewings/archunit/foo/service/FooService.java

package org.littlewings.archunit.foo.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.littlewings.archunit.foo.repository.FooRepository;

@Transactional
@ApplicationScoped
public class FooService {
    @Inject
    FooRepository fooRepository;

    public String message() {
        return fooRepository.get();
    }

    public String messagePlain() {
        return "Hello Foo!!";
    }
}

Repositoryクラス。

src/main/java/org/littlewings/archunit/foo/repository/FooRepository.java

package org.littlewings.archunit.foo.repository;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class FooRepository {
    public String get() {
        return "Hello Foo!!";
    }
}

あくまでArchUnitでのテスト用のクラスなので、中身はあまり気にせず…。

barという方のパッケージは、クラス名などが変わっただけなので割愛します。同じ構成のパッケージを複数用意するためだけのものです。

はじめてのArchUnit

では、ArchUnitを使ってみましょう。最初に作成したテストコードはこちら。

src/test/java/org/littlewings/archunit/ArchUnitHelloWorldTest.java

package org.littlewings.archunit;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import org.junit.jupiter.api.Test;

public class ArchUnitHelloWorldTest {
    @Test
    public void gettingStarted1() {
        JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit");

        // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみの依存関係を許可
        ArchRule rule =
                ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                        .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service..");

        rule.check(classes);
    }

    @Test
    public void gettingStarted2() {
        JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit");

        // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみ呼び出されていること
        ArchRule rule =
                ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                        .should().onlyBeAccessed().byAnyPackage("..resource..", "..service..");

        rule.check(classes);
    }
}

どちらも、serviceパッケージ内のクラスがresourceまたはserviceパッケージからのみ呼び出されていることを確認するテストです。
厳密には少し差異がありますが。

クラスをインポートして

        JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit");

ルールを定義。

        // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみの依存関係を許可
        ArchRule rule =
                ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                        .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service..");

チェック。

        rule.check(classes);

このテストはOKになります。

ここで、FooRepositoryおよびBarRepositoryからServiceクラスにアクセスしてみます。

@ApplicationScoped
public class FooRepository {
    @Inject
    FooService fooService;

    public String get() {
        //return "Hello Foo!!";
        return fooService.messagePlain();
    }
}


@ApplicationScoped
public class BarRepository {
    @Inject
    BarService barService;

    public String get() {
        //return "Hello Bar!!";
        return barService.messagePlain();
    }
}

すると、2つのテストはそれぞれ失敗します。

## gettingStarted1

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..service..' should only have dependent classes that reside in any package ['..resource..', '..service..']' was violated (4 times):
Field <org.littlewings.archunit.bar.repository.BarRepository.barService> has type <org.littlewings.archunit.bar.service.BarService> in (BarRepository.java:0)
Field <org.littlewings.archunit.foo.repository.FooRepository.fooService> has type <org.littlewings.archunit.foo.service.FooService> in (FooRepository.java:0)
Method <org.littlewings.archunit.bar.repository.BarRepository.get()> calls method <org.littlewings.archunit.bar.service.BarService.messagePlain()> in (BarRepository.java:14)
Method <org.littlewings.archunit.foo.repository.FooRepository.get()> calls method <org.littlewings.archunit.foo.service.FooService.messagePlain()> in (FooRepository.java:14)


## gettingStarted2

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in any package ['..service..'] should only be accessed by any package ['..resource..', '..service..']' was violated (2 times):
Method <org.littlewings.archunit.bar.repository.BarRepository.get()> calls method <org.littlewings.archunit.bar.service.BarService.messagePlain()> in (BarRepository.java:14)
Method <org.littlewings.archunit.foo.repository.FooRepository.get()> calls method <org.littlewings.archunit.foo.service.FooService.messagePlain()> in (FooRepository.java:14)

gettingStarted1の方は依存関係を持った時点でNGとなるので、フィールド定義が違反になっています。
メソッド呼び出しの方は、両方のテストケースでNGになっていますね。

OKそうです。

3つのAPI

ArchUnitには、Core API、Lang API、Library APIの3つがあります。

Ideas and Concepts

Core API

Core APIはリフレクションに似たAPIで、クラスやフィールド、メソッドなどを扱います。主に使うのはクラスをインポートする機能に
なるでしょう。

この部分ですね。

        JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit");

サンプル。

src/test/java/org/littlewings/archunit/ImportClassesTest.java

package org.littlewings.archunit;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class ImportClassesTest {
    @Test
    public void importPackages() {
        JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit");
        assertThat(classes.stream().anyMatch(cls -> cls.getSimpleName().equals("JaxrsActivator"))).isTrue();
        assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Resource"))).isTrue();
        assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Service"))).isTrue();
        assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Test"))).isTrue();
    }

    @Test
    public void importPackagesUsingPredefinedImportOptions() {
        JavaClasses classes = new ClassFileImporter()
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                .importPackages("org.littlewings.archunit");

        assertThat(classes.stream().anyMatch(cls -> cls.getSimpleName().equals("JaxrsActivator"))).isTrue();
        assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Resource"))).isTrue();
        assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Service"))).isTrue();
        assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Test"))).isFalse(); // falase
    }
}

クラスをインポートする際に、オプションを指定することもできます。

以下では、JARファイルは対象外、テストクラスも対象外にしています。

        JavaClasses classes = new ClassFileImporter()
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                .importPackages("org.littlewings.archunit");
Lang API

Lang APIは、プリミティブなCore APIの表現力不足を補うものです。

以下のようなものがLang APIですね。

        // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみの依存関係を許可
        ArchRule rule =
                ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                        .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service..");
Library API

Library APIでは、アーキテクチャーを定義してチェックすることができます。

レイヤーを定義して、レイヤー間の依存関係やサイクルチェックなどができます。

Lang APIをもっと見る

Lang APIが以下のような部分だと書きましたが、見ると意味はなんとなくわかるのですが、いざ書こうとすると自信がなくなって
けっこう困ります。

        // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみの依存関係を許可
        ArchRule rule =
                ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                        .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service..");


        // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみ呼び出されていること
        ArchRule rule =
                ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                        .should().onlyBeAccessed().byAnyPackage("..resource..", "..service..");

これを少し分解して見ていきたいと思います。

以下の3つに分解して読めば良さそうです。

ArchRuleDefinition.classes()


.that().resideInAnyPackage("..service..")


.should().onlyBeAccessed().byAnyPackage("..resource..", "..service..");

どう読むかというと。

以下が、「なにが対象か?」を指しています。たとえば、クラスやフィールド、メソッドなどです。

ArchRuleDefinition.classes()

ArchRuleDefinition (archunit 1.0.1 API)

classesconstructorsmembersfieldsmethodscodeUnitsなどを選びます。これらは、対象としたものが
「存在すること」を表します。

noClassesnoConstructorsnoMembersnoFieldsnoMethodsnoCodeUnitsといったnoで始まるものもあり、これらは
「存在しないこと」を表します。

次に、こちらで範囲の絞り込みを行います。

.that().resideInAnyPackage("..service..")

thatで始まり、対象にした要素から以下のインターフェースで絞り込みを行います。どういう絞り込みができるのかは、
各インターフェースのJavadocを読んだ方がよいでしょう。

先ほどの例では、serviceパッケージ(サブクラスを含む)という意味になります。

この..service..という表記は、PackageMatcherに説明があります。

'..pack..' matches 'a.pack', 'a.pack.b' or 'a.b.pack.c.d', but not 'a.packa.b'

PackageMatcher (archunit 1.0.1 API)

記法は他にもいくつかります。

そして、こちらがチェックの内容です。

.should().onlyBeAccessed().byAnyPackage("..resource..", "..service..");

shouldで始まり、以降にチェックの内容が続きます。対象にした要素から以下のインターフェースでチェックの内容を定義します。
こちらも、どのようなチェックができるかは各インターフェースのJavadocを読んだ方がよいでしょう。

サンプル

というわけで、もう少しサンプルを書いてみましょう。

src/test/java/org/littlewings/archunit/ArchUnitExampleTest.java

package org.littlewings.archunit;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.Test;

public class ArchUnitExampleTest {
    static JavaClasses targetClasses =
            new ClassFileImporter()
                            .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
                            .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                            .importPackages("org.littlewings.archunit");

    @Test
    public void naming() {
        // resourceパッケージ内(サブパッケージ含む)のクラス名は、「Resource」で終わること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..resource..")
                        .should().haveSimpleNameEndingWith("Resource")
                        .check(targetClasses);

        // serviceパッケージ内(サブパッケージ含む)のクラス名は、「Service」で終わること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                .should().haveSimpleNameEndingWith("Service")
                .check(targetClasses);

        // repositoryパッケージ内(サブパッケージ含む)のクラス名は、「Repository」で終わること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..repository..")
                .should().haveSimpleNameEndingWith("Repository")
                .check(targetClasses);
    }

    @Test
    public void beanAnnotatedApplicationScoped() {
        // resource、service、repositoryパッケージ内(サブパッケージ含む)のクラスには、@ApplicationScopedアノテーションが付与されていること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..resource..", "..service..", "..repository..")
                .should().beAnnotatedWith(ApplicationScoped.class)
                .check(targetClasses);
    }

    @Test
    public void serviceAnnotatedTransactional() {
        // @Transactionalアノテーションが付与されてOKなのは、serviceパッケージ内(サブパッケージ含む)のクラスのみ
        ArchRuleDefinition.classes().that().areAnnotatedWith(Transactional.class)
                .should().resideInAnyPackage("..service..")
                .check(targetClasses);
        // @Transactionalアノテーションが付与されてOKなのは、serviceパッケージ内(サブパッケージ含む)のクラスのメソッドのみ
        // メソッドでの適用がなくてもOKとする
        ArchRuleDefinition.methods().that().areAnnotatedWith(Transactional.class)
                .should().beDeclaredInClassesThat().resideInAnyPackage("..service..")
                .allowEmptyShould(true)
                .check(targetClasses);

        // serviceパッケージ内(サブパッケージ含む)のクラスには、@Transactionalアノテーションが付与されていること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                .should().beAnnotatedWith(Transactional.class)
                .check(targetClasses);;
    }
}

クラスのインポート。

    static JavaClasses targetClasses =
            new ClassFileImporter()
                            .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
                            .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                            .importPackages("org.littlewings.archunit");

命名規則のチェック。

    @Test
    public void naming() {
        // resourceパッケージ内(サブパッケージ含む)のクラス名は、「Resource」で終わること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..resource..")
                        .should().haveSimpleNameEndingWith("Resource")
                        .check(targetClasses);

        // serviceパッケージ内(サブパッケージ含む)のクラス名は、「Service」で終わること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                .should().haveSimpleNameEndingWith("Service")
                .check(targetClasses);

        // repositoryパッケージ内(サブパッケージ含む)のクラス名は、「Repository」で終わること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..repository..")
                .should().haveSimpleNameEndingWith("Repository")
                .check(targetClasses);
    }

@ApplicationScopedアノテーションが各パッケージのクラスに付与されていることのチェック。

    @Test
    public void beanAnnotatedApplicationScoped() {
        // resource、service、repositoryパッケージ内(サブパッケージ含む)のクラスには、@ApplicationScopedアノテーションが付与されていること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..resource..", "..service..", "..repository..")
                .should().beAnnotatedWith(ApplicationScoped.class)
                .check(targetClasses);
    }

@Transactionalアノテーションは、serviceパッケージ内のクラスのみに付与されていることのチェック。

    @Test
    public void serviceAnnotatedTransactional() {
        // @Transactionalアノテーションが付与されてOKなのは、serviceパッケージ内(サブパッケージ含む)のクラスのみ
        ArchRuleDefinition.classes().that().areAnnotatedWith(Transactional.class)
                .should().resideInAnyPackage("..service..")
                .check(targetClasses);
        // @Transactionalアノテーションが付与されてOKなのは、serviceパッケージ内(サブパッケージ含む)のクラスのメソッドのみ
        // メソッドでの適用がなくてもOKとする
        ArchRuleDefinition.methods().that().areAnnotatedWith(Transactional.class)
                .should().beDeclaredInClassesThat().resideInAnyPackage("..service..")
                .allowEmptyShould(true)
                .check(targetClasses);

        // serviceパッケージ内(サブパッケージ含む)のクラスには、@Transactionalアノテーションが付与されていること
        ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                .should().beAnnotatedWith(Transactional.class)
                .check(targetClasses);;
    }

こんな感じですね。

Library API

Library APIを使うと、レイヤーを定義したりして、さらに高い抽象度でのチェックができるようになります。

The Library API

こんな感じですね。

src/test/java/org/littlewings/archunit/LayeredTest.java

package org.littlewings.archunit;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.library.Architectures;
import org.junit.jupiter.api.Test;

public class LayeredTest {
    @Test
    public void layeredAccess() {
        JavaClasses targetClasses =
                new ClassFileImporter()
                        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
                        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                        .importPackages("org.littlewings.archunit");

        Architectures.LayeredArchitecture layeredArchitecture =
                Architectures.layeredArchitecture()
                        .consideringAllDependencies()
                        .layer("Resource").definedBy("..resource..")
                        .layer("Service").definedBy("..service..")
                        .layer("Repository").definedBy("..repository..")
                        .whereLayer("Resource").mayNotBeAccessedByAnyLayer()
                        .whereLayer("Service").mayOnlyBeAccessedByLayers("Resource", "Service")
                        .whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");

        layeredArchitecture.check(targetClasses);
    }
}

各パッケージに「レイヤー」の名前を付け、その依存関係のチェックを定義しています。

        Architectures.LayeredArchitecture layeredArchitecture =
                Architectures.layeredArchitecture()
                        .consideringAllDependencies()
                        .layer("Resource").definedBy("..resource..")
                        .layer("Service").definedBy("..service..")
                        .layer("Repository").definedBy("..repository..")
                        .whereLayer("Resource").mayNotBeAccessedByAnyLayer()
                        .whereLayer("Service").mayOnlyBeAccessedByLayers("Resource", "Service")
                        .whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");

APIとしては、このあたりを使います。

この例では、Resourceレイヤーはどのレイヤーからもアクセスされてはいけない、
ServiceレイヤーはResourceおよびServiceレイヤーからのアクセスのみ許可、RepositoryレイヤーやServiceレイヤーからの
アクセスのみ許可、といった感じです。

クラスのインポートの部分については、他と同じですね。

JUnitサポートを試す

最後に、ArchUnitのJUnitサポートを試してみます。

JUnit Support

最初に書いたサンプルを、JUnitサポートを使って書き直してみました。

src/test/java/org/littlewings/archunit/ArchUnitHelloWorldJUnit5SupportTest.java

package org.littlewings.archunit;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;

@AnalyzeClasses(
        packages = "org.littlewings.archunit",
        importOptions = {ImportOption.DoNotIncludeJars.class, ImportOption.DoNotIncludeTests.class}
)
public class ArchUnitHelloWorldJUnit5SupportTest {
    @ArchTest
    public static ArchRule onlyDependentServiceFromResource =
            ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                    .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service..");

    @ArchTest
    public static void onlyAccessServiceFromResource(JavaClasses classes) {
        ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                .should().onlyBeAccessed().byAnyPackage("..resource..", "..service..")
                .check(classes);
    }
}

クラスのインポートを@AnalyzeClasses

@AnalyzeClasses(
        packages = "org.littlewings.archunit",
        importOptions = {ImportOption.DoNotIncludeJars.class, ImportOption.DoNotIncludeTests.class}
)
public class ArchUnitHelloWorldJUnit5SupportTest {

ルールの定義を@ArchTestを付与したstaticフィールドまたはメソッドで定義できます。

    @ArchTest
    public static ArchRule onlyDependentServiceFromResource =
            ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                    .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service..");

    @ArchTest
    public static void onlyAccessServiceFromResource(JavaClasses classes) {
        ArchRuleDefinition.classes().that().resideInAnyPackage("..service..")
                .should().onlyBeAccessed().byAnyPackage("..resource..", "..service..")
                .check(classes);
    }

staticフィールドで定義した場合は、@AnalyzeClassesで指定したクラスを対象にチェックの実行までを行ってくれます。

こんなところでしょうか。

まとめ

ArchUnitを試してみました。

実際にソースコードを書く時に、パッケージ間の依存関係やネーミングなど、ルールとしてチェックしたいものを定義、テストできて
よいですね。

Lang APIにちょっと迷いましたが、慣れればなんとかなりそうなので、使えるところでは使っていこうかなと思います。