ArchUnit で Java / Kotlin アプリケーションのアーキテクチャを CI する


(追記)

JJUG CCC 2019 Spring で発表しました。

How to check and improve the Java-based application architecture with “ArchUnit”
https://speakerdeck.com/kawanamiyuu/jjug-ccc-2019-spring


現在、私のチームでは HRTech 系の新規サービスの開発に取り組んでいて、最近 ArchUnit というツールを取り入れましたのでご紹介します。

進化的アーキテクチャという書籍を読んでいて、アーキテクチャの特性の要件準拠度合いを測定 / 担保する適応度関数の 1 つとして、クラスの依存関係をテストできる JDepend が紹介されていて、類似ツールを探してみたところで ArchUnit を発見しました。今年の Technology Radar VOL.19 でも TRIAL として紹介されました。

ArchUnit とは

  • ひとことで言うと、Java / Kotlin アプリケーションのパッケージやクラスの依存関係を JUnit のテストコードして表現しテストできるテストフレームワークです
  • 依存関係の他にも、そのプロダクト固有の命名規則などの実装ルールもテストすることができます
  • はじめてのArchUnit (Qiita) が入門記事としてわかりやすいです

なにが嬉しいか

  • ArchUnit でテストできることは、例えばドキュメントにして周知したり、コードレビューで指摘したりすることで人力でもカバーすることは一応可能です。ただ、人間がチェックする場合、それらを毎回忘れずにできるかというと難しいので、やはり自動化できることには価値があります
  • アーキテクチャやプロダクト固有の実装ルールがコード化されていることで、設計に関する暗黙知が形式知になります
  • 技術力にばらつきがある開発チームで一定の強制力をもってアーキテクチャ設計を担保することができます
  • レガシーコードで、明文化されていない慣習や規約に反している実装を見つけるのに役立ちます

具体例

現在開発中のプロダクトの CI に組み込まれているテストコードの一部をご紹介します。

(1) ドメイン共通の汎用クラスは他の特定ドメインのクラスに依存してはいけない

common パッケージが common でなくなってしまうことを防止します。

※前提として、ドメイン層 (domain パッケージ) 自体の、他の層に対する依存関係は別途実装している Layer Checks テストにより担保されているものとします。


@Test
void domainCommonPackageShouldNotDependOnOtherPackages() {
    noClasses()
        .that().resideInAPackage(ROOT_PACKAGE + ".domain.common..")
        .should()
        .dependOnClassesThat(new DescribedPredicate<>("domain パッケージ下の common パッケージ以外のクラス") {
            @Override
            public boolean apply(JavaClass clazz) {
                return clazz.getPackageName().startsWith(ROOT_PACKAGE + ".domain")
                    && ! clazz.getPackageName().equals(ROOT_PACKAGE + ".domain") // domain パッケージ直下の基底インターフェイス/クラスへの依存は許容
                    && ! clazz.getPackageName().startsWith(ROOT_PACKAGE + ".domain.common");
            }
        })
        .check(CLASSES);
}

(2) JsonSerializable を実装した enum はデシリアライズ方法も実装しなければならない

JSON のプロパティを数値の列挙にデシリアライズする場合に、明示的にデシリアライズ方法を実装しないと、数値に対応する列挙ではなく、列挙の ordinal 値に対応する列挙がインスタンス化されるという JSON ライブラリの仕様に一度ハマったことありました。

同じことに二度ハマらないよう、デシリアライズ方法 (@JsonCreator でアノテートしたファクトリーメソッド) の実装忘れをチェックするテストを追加しました。


@Test
void jsonSerializableShouldImplementJsonCreator() {
        classes()
            .that(new DescribedPredicate<>("JSON シリアライズされる enum") {
                @Override
                public boolean apply(JavaClass clazz) {
                    return clazz.isEnum() && clazz.isAssignableTo(JsonSerializable.class);
                }
            })
            .should(new ArchCondition<>("@JsonCreator でアノテートしたファクトリーメソッドを実装する") {
                @Override
                public void check(JavaClass clazz, ConditionEvents events) {
                    boolean hasJsonCreator = clazz.getMethods().stream()
                        .anyMatch(method -> method.isAnnotatedWith(JsonCreator.class));

                    if (! hasJsonCreator) {
                        events.add(SimpleConditionEvent.violated(
                            clazz,
                            clazz.getName() + " should implement a factory method annotated with @JsonCreator."
                        ));
                    }
                }
            })
            .check(CLASSES);
}

まとめ

いかがでしたでしょうか。アーキテクチャをテストするというツールはこれまであまりなかったように思います。今回紹介した例以外にもアイデア次第でいろいろな観点のテストが実装できそうです。自動テストでアーキテクチャの品質を担保しつつ、プロダクト開発を通じて継続的にアーキテクチャを磨いていくことができるとしたら、、、わくわくしますよね