目次

目次

GitHub Actionsを駆使したカバレッジの自動計測と差分レポート生成

アバター画像
杉山裕哉
アバター画像
杉山裕哉
最終更新日2025/11/25 投稿日2025/06/30

はじめに

NX開発推進部プロダクト開発第1G の杉山です。 Androidエンジニアとして日々開発を行っております。 ここ一ヶ月の週末の内、名古屋を3往復もしており、一体どこ住みなのかと皆に聞かれている次第です。

そろそろ名古屋に別荘でも持つべきかと悩んでいます。

そんなことはさておき、近年社内の取り組みとしてアプリの品質改善を強化しており、リリース前のテストはもちろんのこと日々の開発の中でUT(ユニットテスト)の拡充を行っております。 今回はそのUTのカバレッジをGitHub Actionsを使ってブランチごとに計測し、差分を算出できるようにしました。

背景

差分を算出しようと考えた背景として、UTの作成が十分に意識されず、見逃されたり後回しにされたりする現状があります。この状況を改善し、UT作成への意識をより強化したいと考えました。以前から、PR時にカバレッジの計測は行われており、マージ後のカバレッジを把握することは可能でした。しかし、個々のPRによって具体的にどの程度カバレッジが上昇したのかを把握することができず、UTの実装が不十分だった場合にPRの段階で検出できるようにはなっていませんでした。そのため、GitHub Actionsを用いてこれを改善し、実装しました。

実現したいこと

今回実現したいことは以下の3つになります。

  • PR対象のターゲットブランチ(developブランチ)とソースブランチ(featureブランチ)それぞれのカバレッジを算出する
  • 各ブランチのカバレッジをファイルに保存して参照できるようにする
  • 各ファイルを用いてカバレッジの差分算出をする

上記を実現することで、PRの段階でカバレッジの差分を見てUTの実装ができているかどうかを確認することができ、UT作成への意識付けをすることができます。では、それぞれの実装を紹介していきます。

GitHub Actionsの構成

今回実装したjobの構成は以下になっています。

  • check-coverage-develop
    • developブランチのカバレッジ計測job
  • check-coverage-feature
    • featureブランチのカバレッジ計測job
  • compare-coverage
    • カバレッジ差分計測job

Koverによるカバレッジ計測

カバレッジを計測するにあたってKoverを使用しています。KoverはKotlin 公式のプラグインでKotlinに特化したコードカバレッジツールです。Gradleプラグインに組み込むことができ、クラスや関数レベルで詳細なカバレッジレポートを生成することができます。HTMLとXML形式でレポートを生成することができ、今回はXML形式で出力したものを差分計算に利用しています。 導入に関しては公式のドキュメントがあるため今回は割愛します。

今回導入するプロジェクトではコードはMVVMとクリーンアーキテクチャの設計を取り入れており、ロジックの部分の大半はUseCaseクラスに集約されています。そのためUTに対して特に力をいれる必要のあるクラスになっています。 そこで、今回はUseCaseクラスを対象にカバレッジを計測する設定にしています。(設定については以下に記載します)

kover {
    currentProject {
        createVariant("usecase") {
            add(["developDebug"], false)
        }
    }
    // Usecase用のレポート
    reports {
        variant("usecase") {
            // Usecase用のカバレッジフィルター設定
            filters {
                includes {
                    classes("*UseCase") // UseCaseクラスのカバレッジ
                }
                excludes { // Hiltクラスなど自動生成されるクラスは対象から外しています
                    classes(
                        "*.BuildConfig",
                        "hilt_aggregated_deps.*", "dagger.*", "*_Factory", "*_*Factory*",
                        "*.Hilt_*", "*_HiltModules*",
                        "*_Impl*"
                    )
                }
            }
        }
    }
}

実装1:ブランチ毎にカバレッジを計測する

ブランチ毎にカバレッジを計測するにはjobをそれぞれのブランチを対象に実行条件を切り分ける必要があります。これは ifを用いることで、実現することができます。

  • ターゲットブランチ
  check-coverage-develop:
    runs-on: [ ubuntu-latest ]
    if: github.base_ref == 'develop' # github.base_ref:マージ先がdevelopブランチであること

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          ref: develop # checkout対象をdevelopに指定
          token: ${{ secrets.APP_BOT_GITHUB_TOKENS }}

  • ソースブランチ
check-coverage-feature:
    runs-on: [ ubuntu-latest ]
    if: startsWith(github.head_ref, 'feature/') # github.head_ref:マージ元がfeatureブランチであること

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # checkout対象をソースブランチに指定
          token: ${{ secrets.APP_BOT_GITHUB_TOKENS }}

実装2:各ブランチのカバレッジをファイルに保存する

job毎で内容がリセットされてしまい、このままでは計測したカバレッジが保持できず compare-coverageでカバレッジ差分を計測することができません。 そのため check-coverage-develop,check-coverage-featureのそれぞれで生成したレポートファイルを保存する必要があります。GitHub Actionsではファイルやデータをアーティファクトとして保存することが出来るので今回はこの方法を利用しました。 以下のコードは check-coverage-developのものです。check-coverage-featureも同様の処理を追加しています。

- name: Generate usecase coverage report
        run: ./gradlew koverXmlReportUsecase # Kover側でUseCaseのみを対象としてカバレッジを算出するようにしています
- name: rename coverage report
        run: |
          mv ${{ github.workspace }}/app/build/reports/kover/reportUsecase.xml ${{ github.workspace }}/app/build/reports/kover/reportUsecaseDevelop.xml
      - name: Upload coverage reports as artifacts
        uses: actions/upload-artifact@v4
        with:
          name: coverage-reports-develop
          path: |
            ${{ github.workspace }}/app/build/reports/kover/reportUsecaseDevelop.xml

rename coverage report:保存するファイルがターゲットブランチのものかソースブランチのものか判断できるようにするために、リネームをします Upload coverage reports as artifacts:actions/upload-artifactを使うことでpathで指定した対象ファイルをアーティファクトにアップロードします

実装3:カバレッジの差分を算出する

カバレッジの差分を算出する compare-coveragejobは各ブランチのカバレッジレポートが生成されていないと算出できないため、jobを実行する条件としてcheck-coverage-develop,check-coverage-featureの両jobが正常に完了していることを条件として加えています。これはneedsを利用することで実現可能になります。

compare-coverage:
    runs-on: ubuntu-latest
    needs: [ check-coverage-develop, check-coverage-feature ]  # 2つのジョブが終了してから実行

次に、各ブランチでアップロードしたファイルをダウンロードする必要があります。 ダウンロードには actions/download-artifactを使います。

- name: Download coverage reports from develop
        uses: actions/download-artifact@v4
        with:
          name: coverage-reports-develop  # アーティファクト名

      - name: Download coverage reports from feature
        uses: actions/download-artifact@v4
        with:
          name: coverage-reports-feature  # アーティファクト名

差分の計算はJavaScriptを作成して実行しています。各ブランチのxmlのファイルから対象クラスのカバレッジを合算します。その後、差分を算出してPRコメントに投稿します。

  • 各ブランチのカバレッジ計算処理の一部(以下はdevelopブランチの場合の例を示しています)
import fs from 'fs';
import path from 'path';
import xml2js from 'xml2js';
import { Octokit } from '@octokit/rest';

// GitHubの設定を環境変数から取得
const GITHUB_REPO = process.env.GITHUB_REPOSITORY; 
const GITHUB_TOKEN = process.env.GITHUB_TOKEN; // GitHubのアクセストークン
const PR_NUMBER = process.argv[2]; // コマンドライン引数からPR番号を取得

const developXmlFilePath = path.join(process.env.GITHUB_WORKSPACE, 'reportUsecaseDevelop.xml');  // アーティファクトをダウンロードしてきたファイルパス
const developXmlData = fs.readFileSync(developXmlFilePath, 'utf8'); // ダウンロードファイルの読み込み

parser.parseString(developXmlData, (err, result) => {
    if (err) {
        console.error(err);
        return;
    }

    //クラス数のカウンターを処理
    const counters = result.report.counter;
    const counter = counters[0]

    console.log(`Counter Type: ${counter.$.type} ${counter.$.missed} ${counter.$.covered}`);

    if (counter.$.type === 'INSTRUCTION') {
        const missed = Number(counter.$.missed);
        const covered = Number(counter.$.covered);
        const total = missed+covered;
        developCoverage = (covered / total) * 100; // カバレッジを算出

        // カバレッジデータを報告
        coverageReport += `develop カバレッジ: ${developCoverage.toFixed(2)}% \n`;
    };

});

// featureブランチも同様に計算する

if (developCoverage != 0 && featureCoverage != 0) {
    coverageReport += `${(featureCoverage-developCoverage).toFixed(2)}% \n`; // 両カバレッジが計算できたら差分を算出する。
};

// PRにコメントを追加
const commentBody = coverageReport || 'INSTRUCTIONのカバレッジ情報はありません。';

// GitHub PRコメントの投稿
octokit.issues.createComment({
    owner: GITHUB_OWNER,
    repo: GITHUB_REPO_NAME,
    issue_number: PR_NUMBER,
    body: commentBody,
}).then(() => {
    console.log('Comment posted to PR successfully!');
}).catch(error => {
    console.error('Error posting comment:', error);
});

結果

上記を実装してPR時にGitHub Actionsが実行されると以下のようにカバレッジの差分が算出されます。

スクリーンショット 2025-06-23 17.55.04.png

今回の実装により、実際にUT作成対象のクラスが実装されたPRにてUTの作成漏れによってカバレッジが下がっている事を検知することができました。レビュー時点で指摘してUT作成まで行うことができました。結果として意識付けすることができたと思います。

まとめ

今回はGitHub Actionsを使ってカバレッジをブランチ毎に計測して差分算出できるようにしました。その結果、PR時点で指摘漏れすることのあるUTの作成漏れを検知することができ、UT作成への意識付けを強くすることができました。 今後の展望としては、以下のことが出来るとよりUTへの意識付けが出来るようになると思います。

  • PR対象のクラス毎のカバレッジ差分も出せるようする
  • カバレッジが下がった場合はマージブロックをかけるようにする
アバター画像

杉山裕哉

目次