📝

Kover を使って PR にシンプルなテストカバレッジレポートをつけたい

2024/06/06に公開

こんにちは!アルダグラムでエンジニアをしている内倉です🦐

ユニットテストのカバレッジを出したいけれど「ちょっと見たいだけで Codecov とか使うほどではないんだよな」というようなこと、たまにありませんか?

今まで Rails のプロジェクトでは、テストカバレッジを GitHub の PR にコメントとして追加してみたことがあるのですが、Android + Kotlin ではやったことなかったので、少し調べてみました。

Android でカバレッジ取るなら、JaCoCo 一択なのかな?と思っていましたが、Kotlin だったら Kover というのが良さそうだったので導入してみたところ、出力したレポートをほぼそのままPRコメントにつけるだけでも、結構いい感じになりました。

Kover とは

Kover は、JVM や Android プラットフォーム向けにコンパイルされた Kotlin コードのテストカバレッジを測定するための Gradle プラグインです。HTML or XML のカバレッジレポートを生成することができ、Java が混在するプロジェクトにも対応しています。

JaCoCo と比較すると、Kover は設定がシンプルで、軽量に実行することができるようです。

詳細なレポートを生成したい場合には、Kover だけでは対応がむずかしいこともありそうですが、今回の用途にはぴったりです。

設定してみる

前提条件:

  • マルチモジュールの Android プロジェクト
  • コードは Kotlin のみ
  • Gradleのプラグイン設定は
    • レガシー方式を利用
    • Version Catalog を使用
  • GitHub Actions で、すでにユニットテストを実行している

1. Gradle の設定

公式の quick start を参考にやっていきます💪

  1. 利用する Kover のバージョンを定義します。

    [versions]
    ...
    kover = "0.8.0"
    ...
    
    [libraries]
    ...
    kover-gradle-plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
    ...
    
  2. プロジェクトのルートの build.gradle にプラグインを追加します。

    buildscript {
    		...
        dependencies {
    		    ...
            classpath libs.kover.gradle.plugin
        }
    }
    
  3. アプリケーションモジュールを含む、各モジュールの build.gradle にプラグインを適用します。

    apply plugin: 'org.jetbrains.kotlinx.kover'
    
  4. アプリケーションモジュールの build.gradle に、レポートをマージするために、依存関係の設定を追加します。

    dependencies {
        kover project("core:xxx")
        kover project("feature:xxx")
        ...
        // 3. でプラグインを適用した、すべてのモジュールを記載します
    }
    
  5. ライブラリが自動生成したファイルを除外するための設定を追加する。

    4 の手順の段階で、カバレッジレポートが出力できるようになっています。が、そのままでは HiltDataBinding などのライブラリが生成してくれるクラスもカバレッジ対象に含まれてしまいます。

    プロジェクトで利用しているライブラリに応じて、除外の設定を追加します。

    kover {
        reports {
            filters {
                includes {
                    classes("com.example.*")
                }
                excludes {
                    classes(
                        // Android
                        "*BuildConfig*",
                        // Dagger/Hilt
                        "*_*Factory*",
                        "*_ComponentTreeDeps*",
                        "*Hilt_**",
                        "*HiltWrapper_*",
                        "*_Factory*",
                        "*_GeneratedInjector*",
                        "*_HiltComponents*",
                        "*_HiltModules*",
                        "*_HiltModules_BindsModule*",
                        "*_HiltModules_KeyModule*",
                        "*_MembersInjector*",
                        "*_ProvideFactory*",
                        "*_SingletonC*",
                        "*_TestComponentDataSupplier*",
                        ...
                    )
                }
            }
        }
    }
    

    ここで、クラス名に * ? のワイルドカードを使うことができるのですが、ファイル名やディレクトリ名を指定することはできないので注意が必要です。(参考

  6. レポート作成

    タスクを実行して、レポートが生成できるか確認します。

    ./gradlew koverHtmlReportLocalDebug
    

    レポートは、マージの設定をしたモジュールの build/reports/kover 以下に出力されます。

    今回だと、アプリケーションモジュールでやってるので、 android/app/build/repors/kover/htmlLocalDebug です。XMLのレポートを出力する場合や、タスクを他のビルドバリアントで実行した場合も、同様です。

2. GitHub Actions の設定

GitHub Actions の、ユニットテストを行っている workflow に、カバレッジレポートを出力する処理を追加していきます。

  1. カバレッジレポートから、ほしい部分だけ抜き出すスクリプトを作成する

    まずは、Kover が出してくれたカバレッジレポートのパスを渡したら、必要な部分だけ切り取って返すスクリプトを書きます。

    たいしたことはやらないので、パーサーはお好みで。

    今回は、サマリーと各モジュールごとの結果のテーブルを切り出します。

    const { parse } = require('node-html-parser')
    const fs = require('fs')
    
    // android/app/build/reports/kover/htmlDevDebug/index.html
    const htmlFilePath = process.argv[2]
    if (!htmlFilePath) {
      console.error('file path is required')
      process.exit(1)
    }
    
    const html = fs.readFileSync(htmlFilePath, 'utf-8')
    const root = parse(html)
    
    const coverageStatesTables = root.querySelectorAll('table.coverageStats')
    const result = '<h2>Overall Coverage Summary</h2>'
      + coverageStatesTables[0]
      + '<details><summary><h2>Coverage Detail</h2></summary>' // 各モジュールの結果は、長くなっちゃうので折りたたみにします。
      + coverageStatesTables[1]
      + '</details>'
    
    // リンクはいらないので削除
    console.log(result.replace(/\n/g, '').replace(/<a href="[^"]*">([^<]*)<\/a>/g, '$1'))
    
    
  2. GitHub Actions の workflow に、カバレッジレポート関連の処理を追加する。

    ユニットテストw実行している step の次くらいに、処理を追加していきます。

    ①で、先程作ったスクリプトを実行して、②でPRコメントに出力しています。

    name: Run unit tests for Android
    
    on:
      pull_request:
        branches-ignore:
          - 'master'
        paths:
          - 'android/**'
      workflow_dispatch:
    
    jobs:
      unit_test_android:
        runs-on: ubuntu-20.04
        steps:
    	    ...
    	    - name: Run unit tests
    		    ...
    		  - name: Generate coverage report
            run: |
              ./gradlew koverHtmlReportDevDebug
            working-directory: ./android
          - name: Parse the coverage report # ① カバレッジレポート作成
            run: |
              REPORT=$(node scripts/parse_coverage_results_android.js android/app/build/reports/kover/htmlDevDebug/index.html)
              echo "report=$REPORT" >> $GITHUB_OUTPUT
            id: parse-report
          - name: Comment the coverage report # ② カバレッジレポートからほしい部分を抜き出す
            uses: actions/github-script@v4
            with:
              github-token: ${{ secrets.GITHUB_TOKEN }}
              script: |
                const bodyHeader = '<h2>Overall Coverage Summary</h2>';
                const issue_number = context.issue.number;
                const reportContent = `${{ steps.parse-report.outputs.report }}`;
                const comments = await github.issues.listComments({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: issue_number,
                });
                # ③ 同じPRに対して、workflow が実行されたときは、コメントを上書きします。
                const existingComment = comments.data.find(comment => comment.body.startsWith(bodyHeader));
                if (existingComment) {
                  github.issues.updateComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    comment_id: existingComment.id,
                    body: reportContent,
                  });
                } else {
                  github.issues.createComment({
                    issue_number: issue_number,
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    body: reportContent
                  });
                }
          ...
    

3.完成!!

PR上では、こんなかんじに出力されます🥳

ちょっと確認するには、いいかんじになったんじゃないでしょうか。

まとめ

Kover を使ってAndroid + Kotlin プロジェクトでのテストカバレッジレポート出力し、GitHub の PR にコメントとして追加する方法をご紹介しました。

私は Gradle 自体不慣れなので、ちょっと手間取ってしまいましたが、Kover 自体の設定はほんとにシンプルでした。

今後、カバレッジレポートを活用して、よいプロダクトを作っていきたいと思います。

もっとアルダグラムエンジニア組織を知りたい人、ぜひ下記の情報をチェックしてみてください!

アルダグラム Tech Blog

Discussion