ArchUnit 実践:Layered Architecture のアーキテクチャテスト


// 実行環境
* AdoptOpenJDK 11.0.9.1+1
* JUnit 5.7.0
* ArchUnit 0.14.1

レイヤーの依存関係

Java プロジェクトのパッケージ構成

アーキテクチャテストの実装

package com.example;

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 com.tngtech.archunit.library.Architectures.layeredArchitecture;

class ArchitectureTest {

    // 検査対象のクラス
    private static final JavaClasses CLASSES =
            new ClassFileImporter()
                    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                    .importPackages("com.example");

    @Test
    void レイヤードアーキテクチャのアーキテクチャテスト() {
        layeredArchitecture()
            // presentation パッケージを UI 層として定義
            .layer("ui").definedBy("com.example.presentation..")
            // application パッケージをアプリケーション層として定義
            .layer("app").definedBy("com.example.application..")
            // domain パッケージをドメイン層として定義
            .layer("domain").definedBy("com.example.domain..")
            // infrastructure パッケージをインフラストラクチャ層として定義
            .layer("infra").definedBy("com.example.infrastructure..")

            // UI 層はどの層からも依存されない
            .whereLayer("ui").mayNotBeAccessedByAnyLayer()
            // アプリケーション層は UI 層からのみ依存される
            .whereLayer("app").mayOnlyBeAccessedByLayers("ui")
            // ドメイン層は UI 層、アプリケーション層からのみ依存される
            .whereLayer("domain").mayOnlyBeAccessedByLayers("ui", "app")
            // インフラストラクチャ層は UI 層、アプリケーション層、ドメイン層から依存される
            .whereLayer("infra").mayOnlyBeAccessedByLayers("ui", "app", "domain")

            .check(CLASSES);
    }
}

アーキテクチャテストの実行例(テスト失敗例)

アプリケーション層の Service クラスが、プレゼンテーション層の Helper クラスに依存してしまっている、というアーキテクチャ違反を検知した想定でのテスト失敗例。

$ ./gradlew clean check

> Task :test

ArchitectureTest > レイヤーアーキテクチャ() FAILED
    java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'Layered architecture consisting of
    layer 'ui' ('com.example.presentation..')
    layer 'app' ('com.example.application..')
    layer 'domain' ('com.example.domain..')
    layer 'infra' ('com.example.infrastructure..')
    where layer 'ui' may not be accessed by any layer
    where layer 'app' may only be accessed by layers ['ui']
    where layer 'domain' may only be accessed by layers ['ui', 'app']
    where layer 'infra' may only be accessed by layers ['ui', 'app', 'domain']' was violated (2 times):
    Constructor <com.example.application.employee.EmployeeRegisterService.<init>(com.example.presentation.JsonRenderHelper)> has parameter of type <com.example.presentation.JsonRenderHelper> in (EmployeeRegisterService.java:0)
    Field <com.example.application.employee.EmployeeRegisterService.jsonRenderHelper> has type <com.example.presentation.JsonRenderHelper> in (EmployeeRegisterService.java:0)
        at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:94)
        at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:82)
        at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:267)
        at com.example.ArchitectureTest.レイヤーアーキテクチャ(ArchitectureTest.java:69)

1 test completed, 1 failed

> Task :test FAILED