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

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

## レイヤーの依存関係

基本的な考え方は、23 日目の [ArchUnit 実践：Onion Architecture のアーキテクチャテスト](https://qiita.com/kawanamiyuu/items/0bb30d7094adf93ca763) のオニオンアーキテクチャと同じ。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899/4ce90f90-3db8-40a9-b5d5-3d45eb9ea34f.png)

* Interface Adapters 層と Application Business Rules 層の関係について、制御の方向と依存の方向が逆転している

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899/7acb59d3-7365-7d54-fd4d-457a06f0f74f.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899/c5826c83-c41c-d744-4959-5ba93454da36.png)

### レイヤーとパッケージの対応関係

* Enterprise Business Rules レイヤー
    * domain パッケージ
* Application Business Rules レイヤー
    * application パッケージ
* Interface Adapters レイヤー
    * Controllers アダプター
        * api パッケージ
    * Gateways アダプター
        * persistence パッケージ
    * ~~Presenters アダプター~~
        * :pencil: Web アプリケーションフレームワークに Spring Boot を使用する想定で、レスポンスデータのレンダリングはフレームワークに任せることになるため、実装は省略する

### Controller の実装イメージ

```java
package com.example.api.employee;

import com.example.application.employee.EmployeeEditInputData;
import com.example.application.employee.EmployeeEditOutputData;
import com.example.application.employee.EmployeeEditUseCase;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class EmployeeController {

    private final EmployeeEditUseCase editUseCase;

    public EmployeeController(final EmployeeEditUseCase editUseCase) {
        this.editUseCase = editUseCase;
    }

    @PutMapping("/employees/{id}")
    public ResponseEntity<EmployeeEditResponse> edit(
        @PathVariable final int id,
        @RequestBody final EmployeeEditRequest request
    ) {
        EmployeeEditInputData inputData = request.toInputData(id);

        EmployeeEditOutputData outputData = editUseCase.execute(inputData);

        EmployeeEditResponse response = EmployeeEditResponse.fromOutputData(outputData);

        return ResponseEntity.ok(response);
    }
}
```

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

23 日目の [ArchUnit 実践：Onion Architecture のアーキテクチャテスト](https://qiita.com/kawanamiyuu/items/0bb30d7094adf93ca763) に、Interface Adapters 層と Application Business Rules 層の関係（`ControllerはUseCaseに実処理を委譲する`）を表現するアーキテクチャテストを追加。

```java
package com.example;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.AccessTarget;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;
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()
            // Enterprise Business Rules
            .domainModels("com.example.domain.model..")
            .domainServices("com.example.domain.service..")

            // Application Business Rules
            .applicationServices("com.example.application..")

            // Interface Adapters
            .adapter("controllers", "com.example.api..")
            .adapter("gateways", "com.example.persistence..")

            .check(CLASSES);
    }

    @Test
    void ControllerはUseCaseに実処理を委譲する() {
        classes()
            .that()
            .resideInAPackage("com.example.api..")
            .and().areAnnotatedWith(RestController.class)
            .should(new ArchCondition<>("delegate the process to UseCases") {
                @Override
                public void check(final JavaClass controller, final ConditionEvents events) {
                    JavaConstructor constructor = controller.getConstructors()
                        .stream().findFirst().orElseThrow();
                    boolean isUseCaseInjectedIntoConstructor = constructor.getRawParameterTypes()
                        .stream().anyMatch(this::isUseCase);

                    if (! isUseCaseInjectedIntoConstructor) {
                        // UseCase がコンストラクタインジェクションされていない場合
                        events.add(SimpleConditionEvent.violated(controller, String.format(
                            "`%s` does not have the parameter type of UseCase.", constructor.getFullName())));
                        return;
                    }

                    controller.getMethods().stream()
                        .filter(this::isRequestHandler)
                        .forEach(handler -> {
                            boolean doesHandlerExecuteUseCase = handler.getMethodCallsFromSelf()
                                .stream().anyMatch(this::isUseCase);

                            if (! doesHandlerExecuteUseCase) {
                                // ハンドラーメソッドが UseCase を実行していない場合
                                events.add(SimpleConditionEvent.violated(controller, String.format(
                                    "`%s` does not execute the method of UseCase.", handler.getFullName())));
                            }
                        });
                }

                private boolean isUseCase(final JavaClass clazz) {
                    return isUseCaseInterface(clazz) && clazz.getMethods().stream().anyMatch(this::isUseCaseMethod);
                }

                private boolean isUseCase(final JavaMethodCall methodCall) {
                    return isUseCaseInterface(methodCall.getTargetOwner()) && isUseCaseMethod(methodCall.getTarget());
                }

                private boolean isUseCaseInterface(final JavaClass clazz) {
                    return clazz.getPackageName().startsWith("com.example.application")
                        && clazz.getSimpleName().endsWith("UseCase")
                        && clazz.isInterface();
                }

                private <T extends HasParameterTypes & HasReturnType> boolean isUseCaseMethod(final T method) {
                    return method.getRawParameterTypes().size() == 1
                        && method.getRawParameterTypes().get(0).getSimpleName().endsWith("InputData")
                        && method.getRawReturnType().getSimpleName().endsWith("OutputData");
                }

                private boolean isRequestHandler(final JavaMethod method) {
                    return method.isAnnotatedWith(RequestMapping.class)
                        || method.isAnnotatedWith(GetMapping.class)
                        || method.isAnnotatedWith(PostMapping.class)
                        || method.isAnnotatedWith(PutMapping.class)
                        || method.isAnnotatedWith(PatchMapping.class)
                        || method.isAnnotatedWith(DeleteMapping.class);
                }
            })
            .check(CLASSES);
    }
}
```

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

### テスト失敗例①

Controller クラスが UseCase インターフェイスではなく、その具象クラスに依存していまっている、というアーキテクチャ違反を検知した想定でのテスト失敗例。

```
$ ./gradlew clean check

> Task :test

ArchitectureTest > ControllerはUseCaseに実処理を委譲する() FAILED
    java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package 'com.example.api..' and are annotated with @RestController should delegate the process to UseCases' was violated (1 times):
    `com.example.api.employee.EmployeeController.<init>(com.example.application.employee.EmployeeEditInteractor)` does not have the parameter type of UseCase.
        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.lang.ArchRule$Factory$SimpleArchRule.check(ArchRule.java:198)
        at com.tngtech.archunit.lang.syntax.ObjectsShouldInternal.check(ObjectsShouldInternal.java:81)
        at com.example.ArchitectureTest.ControllerはUseCaseに実処理を委譲する(ArchitectureTest.java:675)

2 tests completed, 1 failed
```

### テスト失敗例②

Controller のハンドラーメソッドが、UseCase のメソッドを実行していない（＝UseCase に処理を委譲していない）、というアーキテクチャ違反を検知した想定でのテスト失敗例。

```
$ ./gradlew clean check

> Task :test

ArchitectureTest > ControllerはUseCaseに実処理を委譲する() FAILED
    java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package 'com.example.api..' and are annotated with @RestController should delegate the process to UseCases' was violated (1 times):
    `com.example.api.employee.EmployeeController.edit(int, com.example.api.employee.EmployeeEditRequest)` does not execute the method of UseCase.
        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.lang.ArchRule$Factory$SimpleArchRule.check(ArchRule.java:198)
        at com.tngtech.archunit.lang.syntax.ObjectsShouldInternal.check(ObjectsShouldInternal.java:81)
        at com.example.ArchitectureTest.ControllerはUseCaseに実処理を委譲する(ArchitectureTest.java:673)

2 tests completed, 1 failed
```