9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

この記事は and factory.inc Advent Calendar 2023 の10日目の記事です。
昨日は @nsym__m さんの「今年やってよかった輪読会の本と進め方」でした。

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

:eyes: Konsistとは

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

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

例えば、 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") }
}

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

:hammer_pick: 導入方法

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

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)
}

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

:page_facing_up: 実際に作成したテスト

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

ViewModelTest

class ViewModelTest {

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

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

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

UiStateTest

class UiStateTest {

    @Test
    fun UiStateinternalで定義する() {
        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 UseCaseusecaseモジュルに配置する() {
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .assertTrue {
                it.resideInModule("core/usecase")
            }
    }
}

UseCase の実装に関しては様々な流派が存在すると思いますが、導入したプロダクトにおいては invoke を用いて一つの処理しか行わないように作っていたので、それを遵守するようなテストを作成しました。
(これは Konsist の Clean Architecture Snippets に記載されていたものを拝借しました。)

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

:arrow_forward: 実行してみる

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

ルール違反しているコードがある場合はテストが失敗します。
メッセージでどのクラスで失敗しているのかが丁寧に出力されるので、一目瞭然でわかりやすいです。:thumbsup:
test_result.png

:stock: まとめ

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

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

9
1
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
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?