ArchUnit 実践：依存関係を逆転した Layered Architecture のアーキテクチャテスト



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

## レイヤーの依存関係

1日目の [ArchUnit 実践：Layered Architecture のアーキテクチャテスト](https://qiita.com/kawanamiyuu/items/6ecb790cde05a9b51098) の Layered Architecture と比較すると、[依存関係が逆転](https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E9%80%86%E8%BB%A2%E3%81%AE%E5%8E%9F%E5%89%87)しており、インフラストラクチャ層より下位の層は技術的詳細と疎になる。また、ドメイン層がどの層にも依存しないため、ドメイン層はビジネスのコアロジックの表現により専念できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899/769a7086-43e5-e7ec-52f0-98c6817a1bbb.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899/9db0b94b-45f8-edc3-4600-7372456b0b40.png)

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

```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()
            .layer("ui").definedBy("com.example.presentation..")
            .layer("app").definedBy("com.example.application..")
            .layer("domain").definedBy("com.example.domain..")
            .layer("infra").definedBy("com.example.infrastructure..")

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

            .check(CLASSES);
    }
}
```

## アーキテクチャテストの実行例（テスト失敗例）

ドメイン層の Service クラスが、インフラストラクチャ層の Repository クラスに依存してしまっている、というアーキテクチャ違反を検知した想定でのテスト失敗例。

依存関係逆転の原則を適用するのであれば、

* ドメイン層は Repository インターフェイスを公開し、それをインフラストラクチャ層で実装する
* ドメイン層の Service クラスは Repository インターフェイスに依存し、Repository の実装クラスは DI により注入される

ことが期待される。

```
$ ./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 only be accessed by layers ['infra']
    where layer 'app' may only be accessed by layers ['infra', 'ui']
    where layer 'domain' may only be accessed by layers ['infra', 'app']
    where layer 'infra' may not be accessed by any layer' was violated (2 times):
    Constructor <com.example.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepository)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepository> in (EmployeeService.java:0)
    Field <com.example.domain.employee.EmployeeService.employeeRepository> has type <com.example.infrastructure.datasource.EmployeeRepository> in (EmployeeService.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:89)

1 test completed, 1 failed

> Task :test FAILED
```