経緯
ある日 AndroiDagashi を見ていたら #290 2023-08-27 に
アーキテクチャをテストできる新ツール、 Konsist
というものが紹介されていて、その GitHub のトップページを眺めてみたら、所属先の Kotlin Multiplatform (KMP) を用いたアプリ開発でよくある作業抜けを CI で検出できそうだと思ったので、まずは自分の学習用サンプルコードで試してみました。
Konsist の紹介として、実装クラスに対する internal の設定抜けを単体テストで検出する
Konsist の GitHub トップページには複数の使用例があり、それを読むだけで大まかな使い方を理解できると思いますが、ここでは internal
の設定抜けを単体テストで検出するための Konsist の書き方を紹介します。
あるアプリではマルチモジュールを導入していて、データの処理をモジュールにまとめています。
データの処理を代表するクラスを ○○RepositoryImpl
、そのインターフェースを ○○Repository
として、インスタンスはインターフェースを指定して Hilt などの DIコンテナから取得する形を取りました。
そうなると、実装クラスである ○○RepositoryImpl
には internal
が付いていることが望ましいです。すべての ○○RepositoryImpl
クラスに internal
が付いているかを Konsist を使い単体テストで確認します。
class ArchitectureTest {
@Test
fun checkAllRepositoryImplsAreInternal() {
Konsist.scopeFromProject() // このプロジェクトの
.classes() // すべてのクラスリストを取得。型は List<KoClassDeclaration>
.withNameEndingWith("RepositoryImpl") // 名前が RepositoryImpl で終わる要素に限定する
.assertTrue { it.hasInternalModifier } // すべての要素に対して internal が付いているか確認する
}
}
KMP モジュールのテストから Konsist を使えるようにする
Konsist は JVM ターゲットからしか使えないので、それを追加して jvmTest
の依存ライブラリとして追加します。
konsist = 'com.lemonappdev:konsist:0.13.0'
kotlin {
sourceSets {
// 略
jvm()
jvmTest.dependencies {
implementation(kotlin("test"))
implementation(libs.konsist)
}
}
}
commonTest
方ではテストクラスから Konsist が使えません。Konsist は内部で kotlin-stdlib-jdk8 を使っていて、それは JVM に依存するからです。
Koin の iOS 向け DI 設定の書き忘れを単体テストで検出する
私の学習用サンプルコードでは DI コンテナに Koin を使っています。Koin で作成されたインスタンスを iOS から使うためには、次のリンク先にあるようにヘルパークラスの実装が必要です。
Kotlin Multiplatform Dependency Injection - Injected Classes
ここで iOS/Android 両方から使う状態ホルダーのクラスがあったとして、それに対応するヘルパクラスの実装を忘れたとします。ひとつのリポジトリを私1人で作る分には iOS 側の UI を作るついでに作れば良いです。しかし KMP と iOS の GitHub リポジトリが別で開発担当者も別の場合、iOS の開発担当者がすぐにその状態ホルターを使い UI を作ることができません。
そこで状態ホルダーのクラスが作られた時点で、対応するヘルパクラスが無ければ単体テストを失敗にして CI を失敗にするようにします。
私の学習用サンプルコードは Redux アーキテクチャを採用していて、状態ホルダーは ActionCreator と Reducer で構成されています。その2つのインスタンスは次のようなコードで、iOS から使えるようにしています。
class HomeViewModelHelper : IosViewModelHelperBase<HomeEvent, HomeAction, HomeState, HomeEffect>() {
// 親の abstract class に Redux に従い iOS 側の UI 状態を更新するための処理が入っている
override val actionCreator: HomeActionCreator by inject()
override val reducer: HomeReducer by inject()
}
Konsist を使い ○○ActionCreator/○○Reducer
と対になる ○○ViewModelHelper
が作られているかをチェックする単体テストを書きます。設置場所は commonTest
ではなく jvmTest
になります。
class ArchitectureTest {
@Test
fun checkAllActionCreatorsAndReducersAreRegisteredInViewModelHelper() {
// 名前が ViewModelHelper で終わるクラスのプロパティの型の名前 Set を作成する。
val registeredClassNames = Konsist.scopeFromProject()
.classes()
.withNameEndingWith("ViewModelHelper")
.flatMap {
it.properties()
}.map {
it.type?.name ?: ""
}.toSet()
// 名前が ActionCreator で終わるクラスと Reducer で終わるクラスの名前 Set を作成する。
val actionCreators = Konsist.scopeFromProject()
.classes()
.withNameEndingWith("ActionCreator")
val reducers = Konsist.scopeFromProject()
.classes()
.withNameEndingWith("Reducer")
val checkClassNames = (actionCreators + reducers).map {
it.name
}.toSet()
// 両者が同じかを確認する
Assert.assertEquals(checkClassNames, registeredClassNames)
}
}
まとめ
単体テストで Konsist を使うことで、Kotlin コードのクラス名や修飾子、プロパティなどが、ガイドラインに沿っているかをチェックすることができます。また KMP においては JVM ターゲットのテストとして作成します。
チーム開発において完璧と思って作成したプルリクが Approved になったのでマージしたところ、あとで作業抜けに気がついて追加でプルリクを作成することがありがちな場合は、Konsist を使った単体テストによるチェック項目にすることで、開発効率を向上できると思いました。