ArchUnit 実践：Onion Architecture のアーキテクチャテスト



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

## レイヤーの依存関係

* 依存の方向は外側の層から内側の層への一方通行
* 一番外側のアダプター同士は依存しない

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899/6bc4b692-b54e-4a04-8444-b3307cd4da3b.png)

https://dzone.com/articles/onion-architecture-is-interesting

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899/678e24ee-4786-e87e-e379-a33599328b93.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.onionArchitecture;

class ArchitectureTest {

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

    @Test
    void オニオンアーキテクチャのアーキテクチャテスト() {
        onionArchitecture()
            // domain.model パッケージをドメインモデル層として定義
            .domainModels("com.example.domain.model..")
            // domain.service パッケージをドメインサービス層として定義
            .domainServices("com.example.domain.service..")
            // application パッケージをアプリケーションサービス層として定義
            .applicationServices("com.example.application..")

            // infrastructure パッケージをインフラストラクチャ・アダプターとして定義
            .adapter("infra", "com.example.infrastructure..")
            // presentation パッケージをユーザインターフェイス・アダプターとして定義
            .adapter("ui", "com.example.presentation..")

            .check(CLASSES);
    }
}
```

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

### テスト失敗例①（ドメインサービス→アプリケーションサービスへの依存）

内側のドメインサービス層の Service クラスが、外側のアプリケーションサービス層の UseCase クラスに依存してしまっている、というアーキテクチャ違反を検知した想定でのテスト失敗例。

```
$ ./gradlew clean check

> Task :test FAILED

ArchitectureTest > オニオンアーキテクチャのアーキテクチャテスト() FAILED
    java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'Onion architecture consisting of
    domain models ('com.example.domain.model..')
    domain services ('com.example.domain.service..')
    application services ('com.example.application..')
    adapter 'infra' ('com.example.infrastructure..')
    adapter 'ui' ('com.example.presentation..')' was violated (2 times):
    Constructor <com.example.domain.service.employee.EmployeeRegisterService.<init>(com.example.application.HogeUseCase)> has parameter of type <com.example.application.HogeUseCase> in (EmployeeRegisterService.java:0)
    Field <com.example.domain.service.employee.EmployeeRegisterService.hogeUseCase> has type <com.example.application.HogeUseCase> 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.tngtech.archunit.library.Architectures$OnionArchitecture.check(Architectures.java:538)
        at com.example.ArchitectureTest.オニオンアーキテクチャのアーキテクチャテスト(ArchitectureTest.java:560)

1 test completed, 1 failed
```

### テスト失敗例②（ユーザインターフェイス・アダプター→インフラストラクチャ・アダプターへの依存）

ユーザインターフェイス・アダプターの Controller クラスが、インフラストラクチャ・アダプターの Repository(Impl) クラスに依存してしまっている、というアーキテクチャ違反を検知した想定でのテスト失敗例。

```
$ ./gradlew clean check

> Task :test FAILED

ArchitectureTest > オニオンアーキテクチャのアーキテクチャテスト() FAILED
    java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'Onion architecture consisting of
    domain models ('com.example.domain.model..')
    domain services ('com.example.domain.service..')
    application services ('com.example.application..')
    adapter 'infra' ('com.example.infrastructure..')
    adapter 'ui' ('com.example.presentation..')' was violated (2 times):
    Constructor <com.example.presentation.employee.EmployeeController.<init>(com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeController.java:0)
    Field <com.example.presentation.employee.EmployeeController.employeeRepository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeController.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.tngtech.archunit.library.Architectures$OnionArchitecture.check(Architectures.java:538)
        at com.example.ArchitectureTest.オニオンアーキテクチャのアーキテクチャテスト(ArchitectureTest.java:560)

1 test completed, 1 failed
```