今回は、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です。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>0.9.3</version>
<scope>test</scope>
</dependency>
実際にテストクラスを作成するときは、以下のように@RunWithアノテーションを使ってArchUnitRunnerを読み込んでください。また、テスト対象のアプリケーションのベースパッケージを@AnalyzeClassesアノテーションで指定し、テストメソッドには@ArchTest
を付与します。
@RunWith(ArchUnitRunner::class)
@AnalyzeClasses(packages = ["com.dais39.sample.app"])
class TestApplicationRules {
@ArchTest
fun test(classes: JavaClasses) {
// テスト処理
}
}
パッケージ間の依存関係のテスト
パッケージの依存関係をテストする場合は、以下のようなコードを書きます。
ArchUnitでは、基本的なテストの流れとして、rule
をメソッドチェーンで記述していき、最後にcheck()
を呼びだすことでテストを行います。メソッドを繋げることで表現力のあるコードを書くことができるのがArchUnitの特徴です。
下の例では、applicationパッケージに配置されたクラスを参照するクラスは全て、presentationもしくはapplicationパッケージに配置されていることを保証します。別のパッケージから参照されている場合はテストが失敗します。
@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()
を使うことでクラス名を直接指定することができます。これ以外にも様々なパターンマッチを行うメソッドが用意されています。
@ArchTest
fun ServiceクラスはControllerクラスからのみアクセスされる(classes: JavaClasses){
val rules = ArchRuleDefinition.classes().that().haveNameMatching(".*Service")
.should().onlyBeAccessed().byClassesThat().haveNameMatching(".*Controller")
rules.check(classes)
}
クラス配置に関するテスト
あるクラスが特定のパッケージに配置されていることを保証するテストは以下のように記述します。
@ArchTest
fun ToDoServiceで始まる名前のクラスはapplicationパッケージに配置される(classes: JavaClasses){
val rules = ArchRuleDefinition.classes().that().haveSimpleNameStartingWith("ToDoService")
.should().resideInAPackage("..application..")
rules.check(classes)
}
継承,実装関係のテスト
あるインターフェースが決まった条件のクラスからのみ実装されていることを保証するテストは以下のように記述します。下の例ではhaveSimpleNameEndingWith()
を使って指定した文字列で終わるクラスを条件としています。
@ArchTest
fun ToDoServiceImplはToDoServiceインターフェースを実装する(classes: JavaClasses){
val rules = ArchRuleDefinition.classes().that().implement(ToDoService::class.java)
.should().haveSimpleNameEndingWith("ToDoServiceImpl")
rules.check(classes)
}
アノテーションが付与されたクラスの依存関係のテスト
ToDoServiceクラスが@RestController
が付与されたクラスからのみアクセスされることを保証するテストが以下です。
@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(依存関係逆転の原則)を適用したアーキテクチャを保証するテストが以下です。
@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()
で対象のパッケージを指定します。
@ArchTest
fun 循環参照が存在しない(classes: JavaClasses){
val rules = SlicesRuleDefinition.slices().matching("com.dais39.sample.app.(*)..").should().beFreeOfCycles()
rules.check(classes)
}
おわりに
今回は、ArchUnitとはなにか?ArchUnitを使ってどんなことができるのか?ということを紹介しました。アーキテクチャのテストができる点とKotlinをサポートしている点で、個人的にとても推しているライブラリなので、この記事を見て興味を持つ方が増えるとうれしいです。
参考資料
- ArchUnit https://www.archunit.org/
- TNG/ArchUnit https://github.com/TNG/ArchUnit
- ArchUnit User Guide https://www.archunit.org/userguide/html/000_Index.html
- Technology Radar https://www.thoughtworks.com/radar
- 進化的アーキテクチャ https://www.oreilly.co.jp/books/9784873118567/