はじめに
この記事は and factory.inc Advent Calendar 2023 の10日目の記事です。
昨日は @nsym__m さんの「今年やってよかった輪読会の本と進め方」でした。
本記事では、Kotlin用の静的コード分析ツールである「Konsist」を試しに導入してみたので、導入方法や実際に作成した内容を共有したいと思います。
Konsistとは
「Konsist」は、Kotlin専用の静的コード分析ツールです。コードの一貫性とコーディング規約の遵守を目指し、Kotlinプロジェクトの統一されたコード構造を維持します。このツールは、JUnitやKotestを使用した単体テストの形式で、コード品質を確保するためのチェックを行います。
(ChatGPTによる要約 )
単体テストの形式でコードのルールをチェックしてくれるもので、宣言周りやアーキテクチャ周りのチェックが可能となっています。
例えば、 General Snippets から一つ例を見てみると、「Files In ext Package Must Have Name Ending With Ext( ext
パッケージ内のファイルは Ext
で終わる必要がある)」 というルールがあった場合、下記のようにテストを記述することができます。
@Test
fun `files in "ext" package must have name ending with "Ext"`() {
Konsist
.scopeFromProject()
.files
.withPackage("..ext..")
.assertTrue { it.hasNameEndingWith("Ext") }
}
宣言的な書き味で、テスト自体がわかりやすい印象ですね。
導入方法
導入は非常に簡単で、ライブラリの定義を追加するだけで大丈夫でした。
dependencies {
testImplementation("com.lemonappdev:konsist:0.13.0")
}
※ Version Catalogを使っている場合は↓のような感じ。
[versions]
konsist = "0.13.0"
[libraries]
konsist = { group = "com.lemonappdev", name = "konsist", version.ref = "konsist" }
dependencies {
testImplementation(libs.konsist)
}
( 本記事作成時点では絶賛開発段階らしいので、今後大きな変更などが加わる可能性があり注意が必要です。)
実際に作成したテスト
実際にプロダクトに入れてみたテストを一部記載したいと思います。
ViewModelTest
class ViewModelTest {
@Test
fun ViewModelはinternalで定義する() {
Konsist
.scopeFromProject()
.classes()
.withAllParentsOf(ViewModel::class)
.assertTrue {
it.hasInternalModifier
}
}
}
導入したプロダクトはマルチモジュール構成になっており、各画面を feature
モジュールとして分けています。
各画面に紐づく ViewModel
は、他のモジュールから見える必要はないので internal
で定義すべきです。
そのため ViewModel
に internal
修飾子が宣言されているかをチェックするテストを作成しました。
hasInternalModifier
で internal
修飾子が宣言されているかをチェックできるので非常に楽です。
UiStateTest
class UiStateTest {
@Test
fun UiStateはinternalで定義する() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UiState")
.filter { it.name != "ExceptionAlertUiState" }
.assertTrue {
it.hasInternalModifier
}
}
}
導入したプロダクトはJetpack Composeで画面構築しており、 UiState
で画面の状態を管理しています。
ViewModel
と同様に、他のモジュールから見える必要はないので internal
修飾子が宣言されているかをチェックするテストを作成しました。
ただ、エラーハンドリング関連の UiState
は共通モジュールに定義しており、それに関しては internal
でなくても問題ないので filter
を用いて除外するようにしています。
UseCaseTest
class UseCaseTest {
@Test
fun UseCaseの公開メソッドはinvokeのみにする() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue {
val hasSingleInvokeOperatorMethod = it.hasFunction { function ->
function.name == "invoke" && function.hasPublicOrDefaultModifier && function.hasOperatorModifier
}
hasSingleInvokeOperatorMethod && it.countFunctions { item -> item.hasPublicOrDefaultModifier } == 1
}
}
@Test
fun UseCaseはusecaseモジュールに配置する() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue {
it.resideInModule("core/usecase")
}
}
}
UseCase
の実装に関しては様々な流派が存在すると思いますが、導入したプロダクトにおいては invoke
を用いて一つの処理しか行わないように作っていたので、それを遵守するようなテストを作成しました。
(これは Konsist の Clean Architecture Snippets に記載されていたものを拝借しました。)
また、 UseCase
は core/usecase
モジュールに配置しているので、それのテストも作成しています。
RepositoryTest
class RepositoryTest {
@Test
fun Repositoryのインターフェースはdomainモジュールに配置する() {
Konsist
.scopeFromProject()
.interfaces()
.withNameEndingWith("Repository")
.assertTrue {
it.resideInModule("core/domain")
}
}
@Test
fun Repositoryの実装クラスはdataモジュールに配置する() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("RepositoryImpl")
.assertTrue {
it.resideInModule("core/data")
}
}
@Test
fun Repositoryの実装クラスはinternalで定義する() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("RepositoryImpl")
.assertTrue {
it.hasInternalModifier
}
}
@Test
fun Repositoryの実装クラスはRepositoryImplというsuffixにする() {
Konsist
.scopeFromProject()
.classes()
.withInterface { it.name.endsWith("Repository") }
.assertTrue {
it.name.endsWith("RepositoryImpl")
}
}
}
Repository
に関しては、インターフェースはドメイン層( core/domain
)・実装クラスはインフラ層( core/data
)に配置しているため、正しく配置されているかのテストを作成しています。
また、Repository
の実装クラスのネーミングに関しては Impl
という接尾辞を付けているのでそれのテストも作成しています。
実行してみる
テストとして作成しているので、普通にテストを実行すればチェックが走ります。この点はCI/CDに組み込みやすいのでいいですね。
ルール違反しているコードがある場合はテストが失敗します。
メッセージでどのクラスで失敗しているのかが丁寧に出力されるので、一目瞭然でわかりやすいです。
まとめ
簡単ではありますが、 Konsist を導入してみた話を共有しました。
Konsist 自体機能が豊富なので、まだまだできることはたくさんある印象です。
様々なルールを定義し、プロジェクトを健全に保ったまま素敵なアプリを作っていきたいですね。