ArchUnit 実践:ドメイン層で発生した例外は UI 層やアプリ層で捕捉しきる(大域例外ハンドラで捕捉しない)


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

アーキテクチャテストのモチベーション

ドメイン固有の例外は、そのユースケースを制御する責務をもつコントローラーやアプリケーションサービスで捕捉し、適切な後処理を行うべき。

Spring Boot でいうと、@ExceptionHandler を付与したメソッド(大域例外ハンドラ)が、ドメイン層の例外を捕捉しないことを担保したい。

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

package com.example;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
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 org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.List;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;

class ArchitectureTest {

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

    @Test
    void ドメイン層で発生した例外を大域例外ハンドラで捕捉しない() {
        methods().that().areAnnotatedWith(ExceptionHandler.class)
            .should()
            .notHaveRawParameterTypes(new DescribedPredicate<>("ドメイン層の例外クラス") {
                /**
                 * @param params `@ExceptionHandler` でアノテートされたメソッドの引数のリスト
                 * @return 引数にドメイン層の例外クラスを含む場合、true
                 */
                @Override
                public boolean apply(final List<JavaClass> params) {
                    JavaClass exceptionClass = params.stream()
                            .filter(clazz -> clazz.isAssignableTo(Exception.class))
                            .findFirst()
                            .orElseThrow();

                    return exceptionClass.getPackageName().startsWith("com.example.domain");
                }
            })
            .check(CLASSES);
    }
}