34
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

はじめてのArchUnit

Last updated at Posted at 2018-12-03

今回は、2018/11に発表されたTechnology Radar Vol.19の中で自分が特に気になったArchUnitというライブラリについて書いていきたいと思います。

ゴール

  • ArchUnitとはなにかを知る
  • ArchUnitで出来ることを知る

はじめに

この記事は、公式の情報を参考にしながら実際に手を動かした結果を載せていますが、ArchUnitをつかって実装する場合はこちらの記事ではなく、公式のArchUnit User Guideを参考にするようにしてください。
また、記事の中でなにか間違っている箇所がありましたらコメントお願いします。

ArchUnitとは

ArchUnitは、Java/Kotlinで書かれたアプリケーションを対象とした、アーキテクチャのテストを行うライブラリです。ArchUnitのソースコードはGitHubに公開されています。
https://github.com/TNG/ArchUnit

ArchUnitでは、パッケージやクラス間の依存関係、クラスのパッケージングのチェック、循環参照のチェックなどアーキテクチャに関する様々なテストを行うことができます。
また、ArchUnitは、進化的アーキテクチャで述べられている「適応度関数」を簡単に実装できるライブラリとしてTechnology Radarで紹介されています。適応度関数とは、アーキテクチャの特性を測るため指標です。ArchUnitを使ったテストをCI/CDに組み込むことで適応度関数を実現することができます。適応度関数について詳しく知りたい方は、進化的アーキテクチャTechnology Radar Vol.18を参照してください。

ArchUnitではどんなことができるのか

ここからは実際にコードベースでArchUnitを使ったテストを紹介していきたいと思います。
ArchUnit User Guideを参考にしているので、詳細を知りたい場合はこちらを必ず参照してください。
動作環境は、Maven 3.5.4, Spring Boot 2.1.0, Kotlin 1.3, JUnit 4を想定しています。

導入

JUnit4/Mavenの場合は、pom.xmlに以下を追加すればOKです。

pom.xml
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit4</artifactId>
    <version>0.9.3</version>
    <scope>test</scope>
</dependency>

実際にテストクラスを作成するときは、以下のように@RunWithアノテーションを使ってArchUnitRunnerを読み込んでください。また、テスト対象のアプリケーションのベースパッケージを@AnalyzeClassesアノテーションで指定し、テストメソッドには@ArchTestを付与します。

Sample.kt
@RunWith(ArchUnitRunner::class)
@AnalyzeClasses(packages = ["com.dais39.sample.app"])
class TestApplicationRules {

    @ArchTest
    fun test(classes: JavaClasses) {
        // テスト処理
    }
}

パッケージ間の依存関係のテスト

パッケージの依存関係をテストする場合は、以下のようなコードを書きます。
ArchUnitでは、基本的なテストの流れとして、ruleをメソッドチェーンで記述していき、最後にcheck()を呼びだすことでテストを行います。メソッドを繋げることで表現力のあるコードを書くことができるのがArchUnitの特徴です。
下の例では、applicationパッケージに配置されたクラスを参照するクラスは全て、presentationもしくはapplicationパッケージに配置されていることを保証します。別のパッケージから参照されている場合はテストが失敗します。

Sample.kt
@RunWith(ArchUnitRunner::class)
@AnalyzeClasses(packages = ["com.dais39.sample.app"])
class TestApplicationRules {

    @ArchTest
    fun applicationレイヤのクラスはpresentationレイヤ以外のクラスから依存されない(classes: JavaClasses) {

        val rules = ArchRuleDefinition.classes().that().resideInAPackage("..application..")
            .should().onlyBeAccessed().byClassesThat().resideInAPackage("..presentation..")

        rules.check(classes)
    }
}

クラス間の依存関係のテスト

ServiceクラスがControllerクラスからのみアクセスされることを保証したい場合のテストは以下です。haveNameMatching()を使うと正規表現でパターンマッチができますが、haveSimpleName()を使うことでクラス名を直接指定することができます。これ以外にも様々なパターンマッチを行うメソッドが用意されています。

Sample.kt
@ArchTest
fun ServiceクラスはControllerクラスからのみアクセスされる(classes: JavaClasses){
        
    val rules = ArchRuleDefinition.classes().that().haveNameMatching(".*Service")
            .should().onlyBeAccessed().byClassesThat().haveNameMatching(".*Controller")

    rules.check(classes)
}

クラス配置に関するテスト

あるクラスが特定のパッケージに配置されていることを保証するテストは以下のように記述します。

Sample.kt
@ArchTest
fun ToDoServiceで始まる名前のクラスはapplicationパッケジに配置される(classes: JavaClasses){

    val rules = ArchRuleDefinition.classes().that().haveSimpleNameStartingWith("ToDoService")
            .should().resideInAPackage("..application..")

    rules.check(classes)
}

継承,実装関係のテスト

あるインターフェースが決まった条件のクラスからのみ実装されていることを保証するテストは以下のように記述します。下の例ではhaveSimpleNameEndingWith()を使って指定した文字列で終わるクラスを条件としています。

Sample.kt
@ArchTest
    fun ToDoServiceImplToDoServiceインタフェスを実装する(classes: JavaClasses){

        val rules = ArchRuleDefinition.classes().that().implement(ToDoService::class.java)
            .should().haveSimpleNameEndingWith("ToDoServiceImpl")

        rules.check(classes)
    }

アノテーションが付与されたクラスの依存関係のテスト

ToDoServiceクラスが@RestControllerが付与されたクラスからのみアクセスされることを保証するテストが以下です。

Sample.kt
@ArchTest
fun ToDoServiceクラスはRestControllerアノテションが付与されたクラスからのみアクセスされる(classes: JavaClasses){

    val rules = ArchRuleDefinition.classes().that().areAssignableTo(ToDoService::class.java)
            .should().onlyBeAccessed().byClassesThat().areAnnotatedWith(RestController::class.java)

    rules.check(classes)
}

レイヤアーキテクチャに則ったパッケージ構成のテスト

ArchUnitではレイヤアーキテクチャに則ったパッケージ構成を保証するテストを書くことができます。現状ではレイヤアーキテクチャのみに対応していますが、今後はヘキサゴナルアーキテクチャやクリーンアーキテクチャなどにも対応していくそうです。
レイヤアーキテクチャにDIP(依存関係逆転の原則)を適用したアーキテクチャを保証するテストが以下です。

Sample.kt
@ArchTest
fun アプリケションの構造がレイヤアキテクチャに則っている(classes: JavaClasses){
    val rules = Architectures.layeredArchitecture()
            .layer("Presentation").definedBy("..presentation..")
            .layer("Application").definedBy("..application..")
            .layer("Domain").definedBy("..domain..")
            .layer("Infrastructure").definedBy("..infra..")

            .whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
            .whereLayer("Application").mayOnlyBeAccessedByLayers("Presentation")
            .whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
            .whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer()

    rules.check(classes)
}

循環参照のテスト

アプリケーションに循環参照が存在していないかをチェックするテストを作る場合は、以下のように記述します。matching()で対象のパッケージを指定します。

Sample.kt
@ArchTest
fun 循環参照が存在しない(classes: JavaClasses){

    val rules  = SlicesRuleDefinition.slices().matching("com.dais39.sample.app.(*)..").should().beFreeOfCycles()

    rules.check(classes)
}

おわりに

今回は、ArchUnitとはなにか?ArchUnitを使ってどんなことができるのか?ということを紹介しました。アーキテクチャのテストができる点とKotlinをサポートしている点で、個人的にとても推しているライブラリなので、この記事を見て興味を持つ方が増えるとうれしいです。

参考資料

34
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?