【Android】Kotlin用の静的コード分析ツール「Konsist」を導入してみた

# はじめに
この記事は [and factory.inc Advent Calendar 2023](https://qiita.com/advent-calendar/2023/andfactory) の10日目の記事です。
昨日は @nsym__m さんの「[今年やってよかった輪読会の本と進め方](https://qiita.com/nsym__m/items/fa24d9eed123dc8f1a95)」でした。

本記事では、Kotlin用の静的コード分析ツールである「Konsist」を試しに導入してみたので、導入方法や実際に作成した内容を共有したいと思います。

# :eyes: Konsistとは

https://github.com/LemonAppDev/konsist

> 「Konsist」は、Kotlin専用の静的コード分析ツールです。コードの一貫性とコーディング規約の遵守を目指し、Kotlinプロジェクトの統一されたコード構造を維持します。このツールは、JUnitやKotestを使用した単体テストの形式で、コード品質を確保するためのチェックを行います。
（ChatGPTによる要約 :robot: ）

単体テストの形式でコードのルールをチェックしてくれるもので、宣言周りやアーキテクチャ周りのチェックが可能となっています。

例えば、 [General Snippets](https://docs.konsist.lemonappdev.com/inspiration/snippets/general-snippets) から一つ例を見てみると、**「Files In ext Package Must Have Name Ending With Ext（ `ext` パッケージ内のファイルは `Ext` で終わる必要がある）」** というルールがあった場合、下記のようにテストを記述することができます。

```kotlin
@Test
fun `files in "ext" package must have name ending with "Ext"`() {
    Konsist
        .scopeFromProject()
        .files
        .withPackage("..ext..")
        .assertTrue { it.hasNameEndingWith("Ext") }
}
```

宣言的な書き味で、テスト自体がわかりやすい印象ですね。

# :hammer_pick: 導入方法
導入は非常に簡単で、ライブラリの定義を追加するだけで大丈夫でした。

```gradle
dependencies {
    testImplementation("com.lemonappdev:konsist:0.13.0")
}
```

※ Version Catalogを使っている場合は↓のような感じ。

```toml
[versions]
konsist = "0.13.0"

[libraries]
konsist = { group = "com.lemonappdev", name = "konsist", version.ref = "konsist" }
```

```gradle
dependencies {
    testImplementation(libs.konsist)
}
```

（:warning: 本記事作成時点では絶賛開発段階らしいので、今後大きな変更などが加わる可能性があり注意が必要です。）

# :page_facing_up: 実際に作成したテスト
実際にプロダクトに入れてみたテストを一部記載したいと思います。

## ViewModelTest

```kotlin
class ViewModelTest {

    @Test
    fun ViewModelはinternalで定義する() {
        Konsist
            .scopeFromProject()
            .classes()
            .withAllParentsOf(ViewModel::class)
            .assertTrue {
                it.hasInternalModifier
            }
    }
}
```

導入したプロダクトはマルチモジュール構成になっており、各画面を `feature` モジュールとして分けています。
各画面に紐づく `ViewModel` は、他のモジュールから見える必要はないので `internal` で定義すべきです。
そのため `ViewModel` に `internal` 修飾子が宣言されているかをチェックするテストを作成しました。

`hasInternalModifier` で `internal` 修飾子が宣言されているかをチェックできるので非常に楽です。

## UiStateTest

```kotlin
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

```kotlin
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](https://docs.konsist.lemonappdev.com/inspiration/snippets/clean-architecture-snippets#3.-classes-with-usecase-suffix-should-have-single-public-operator-method-named-invoke) に記載されていたものを拝借しました。）

また、 `UseCase` は `core/usecase` モジュールに配置しているので、それのテストも作成しています。

## RepositoryTest

```kotlin
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` という接尾辞を付けているのでそれのテストも作成しています。

# :arrow_forward: 実行してみる
テストとして作成しているので、普通にテストを実行すればチェックが走ります。この点はCI/CDに組み込みやすいのでいいですね。

ルール違反しているコードがある場合はテストが失敗します。
メッセージでどのクラスで失敗しているのかが丁寧に出力されるので、一目瞭然でわかりやすいです。:thumbsup:
![test_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/f742125d-3587-0b45-195f-5b7caf9c2022.png)


# :stock: まとめ
簡単ではありますが、 Konsist を導入してみた話を共有しました。
Konsist 自体機能が豊富なので、まだまだできることはたくさんある印象です。

様々なルールを定義し、プロジェクトを健全に保ったまま素敵なアプリを作っていきたいですね。