ArchUnit 実践：特定のメソッド呼び出しを禁止する

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

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

タイムゾーンを考慮する必要があるアプリケーション[^1]で、以下の実装ルールに準拠していることを担保したい。

* 原則として `ZonedDateTime`, `OffsetDateTime` の使用を強制したい
* ただし、実装の都合で LocalDateTime インスタンスを扱いたい場合があり、LocalDateTime 型への依存自体は許容したい
* LocalDateTime インスタンスをアプリコード内で生成する必要はなく、LocalDateTime インスタンスの生成方法のうち、最低限、`LocalDateTime.now()`, `LocalDateTime.of(...)`, `LocalDateTime.parse(...)` が使用されないことを担保したい

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

```java
package com.example;
　
import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget;
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 java.time.LocalDateTime;

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

class ArchitectureTest {

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

    @Test
    void doNotInstantiateLocalDateTime() {
        methods().should(new ArchCondition<>("not instantiate LocalDateTime") {
            @Override
            public void check(final JavaMethod method, final ConditionEvents events) {
                method.getMethodCallsFromSelf().forEach(javaMethodCall -> {
                    // 呼び出したメソッド
                    MethodCallTarget calledMethod = javaMethodCall.getTarget();
                    // 呼び出したメソッドが定義されたクラス
                    JavaClass calledClass = calledMethod.getOwner();

                    // LocalDateTime の生成か
                    boolean isLocalDateTimeInstantiated =
                        calledClass.isAssignableTo(LocalDateTime.class)
                            && (
                                calledMethod.getName().equals("now")
                                    || calledMethod.getName().equals("of")
                                    || calledMethod.getName().equals("parse")
                            );

                    if (isLocalDateTimeInstantiated) {
                        // 実装ルール違反を通知
                        events.add(SimpleConditionEvent.violated(
                            method,
                            String.format("`%s` calls `%s`.", method.getFullName(), calledMethod.getFullName()))
                        );
                    }
                });
            }
        })
        .check(CLASSES);
    }
}
```

## アーキテクチャテストの失敗例

```
ArchitectureTest > doNotInstantiateLocalDateTime() FAILED
    java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'methods should not instantiate LocalDateTime' was violated (4 times):
    `com.example.domain.DomainService.hoge1()` calls `java.time.LocalDateTime.now()`.
    `com.example.domain.DomainService.hoge2()` calls `java.time.LocalDateTime.of(java.time.LocalDate, java.time.LocalTime)`.
    `com.example.domain.DomainService.hoge3()` calls `java.time.LocalDateTime.of(int, int, int, int, int, int)`.
    `com.example.domain.DomainService.hoge4()` calls `java.time.LocalDateTime.parse(java.lang.CharSequence)`.
```

[^1]: [タイムゾーンを考慮した日時の扱いのベストプラクティス - エムスリーテックブログ](https://www.m3tech.blog/entry/timezone-handling)