👷

GitHub Actionsでカバレッジを可視化する

12 min read

この記事は LITALICO Engineers Advent Calendar 2020の12/9配信になります。

モチベーション

今携わっているプロダクトは、DDD+Clean Architectureなマイクロサービスとして構築しています。
自身の担当業務としてはレセプト関係になります。業務の特性上、複雑なビジネスルールをきっちりかっちり作らないといけないのでテストはかなり重点的に書いています。
ビジネスルール、ユースケースといったレイヤー化されたクラスにそれぞれテストを書いていくわけですが、ユースケースが依存しているビジネスルールが一つの場合など、いちいちユースケース単体でテストを書くのか?ということもあり、ケースによりますがテスト粒度を上げるという判断もしてたりします。
テストコーディング時に依存性を排除する為にモックを大量に使いますが、上記のような判断があったり、なかったりするとテストのカバー範囲の認識齟齬というものが出てきて最終的にカバレッジが漏れる事案が発生します。
またPRレビュー時にテストコードからモック範囲をちゃんと理解するには、それなりの時間もかかります。
Clean Architectureで単体テストがしやすいメリットはありますが、疎結合が故にクラスが増えてモックも大量に必要になりがちなので結構大変。。テストレビューは人類には難しい。。

じゃあどうすんの?というのは、コードカバレッジを見ていくことになります。
ただコードカバレッジというのは取得するのに時間がかかるし、一々ローカルでやるのかというと正直メンドイというのがありました。

カバレッジレポートツールの選定

まずローカルでカバレッジを取得してもチームメンバーに共有できないので論外です。
チーム内で共有して確認できるレポートツールが必要です。
理想的にはCodecovとかの外部サービスを使えればいいなーとは思いましたが、コストがかかるものでチーム全体で使うのならまだしも1チームで気軽に始めるには若干ハードルが高いし、Jenkinsにplug-in入れて構築してもCIと連動させて結果をその場で見れないと使われないだろーなと感じました。

プロダクトのCIとしてはCircleCIを使っていましたが、その当時GitHub Actionsの紹介記事が結構でてきた時期だったので試して見る価値はありと判断してやってみました。
CircleCIにジョブを増やすと、キュー数を圧迫して既存のジョブの実行待ちが出てきそうだったというのもあり、GitHub Actionsなら気にせず、だいたい無料で使えて気軽に始められるというのも大きなメリットとして感じました。

できたもの

Pull Request(以下、PR)を作るとGitHub Actionsのワークフローが起動しカバレッジレポートを以下の様にPRのコメントとして投稿されます。

PHPUnitカバレッジ
PHPUnitカバレッジ例

jestカバレッジ
jestカバレッジ例
(フロントエンドは最近jestを導入したばかりなのでカバレッジ率はまだこれからですw)

見ていただいて分かる通りGitHub ActionsがPRコメントとしてカバレッジのサマリーとレポートのリンクを投稿しています。
プロダクトコードに追加・修正があった場合に、該当のソースに対応したカバレッジのリンクをつける様にしています。リンクから詳細レポートをそのまま確認できます。
再プッシュされたら再度ワークフローが動き、先にコメントされたものが更新されます。

ワークフロー定義

GitHub ActionsのワークフローのサンプルコードはGistにアップしているので、良かったら見てみてください。

コードの説明はDB・APIといった外部サービスに依存しない分シンプルなjest版をベースに説明したいと思います。
PHPUnit版はjestとやっていることは大幅に変わりはありません。

GitHub Actions上でテストを動かすこと自体(PHPUnitやjestの設定自体)については今回の趣旨から外れるので細かくは説明しません。記事の最後に参考URLを載せていますので、そちらからどうぞ。
またアプリケーションのディレクトリ構成[1][2]だったりは適宜読み替えてください。

ワークフロー解説

  • ワークフロー宣言
1-7行目
name: test with code coverage
on:
  pull_request:
    types: [opened, reopened, synchronize]
    paths:
      - '**.vue'
      - '**.js'

name はワークフロー名。
on はどのタイミングでこのワークフローを実行するか指定する場所です。
今回はPRコメントを付けたいので push ではなく pull_request を指定する必要がありました。
typespaths で更に条件を絞ってワークフローを制御することができます。
特にテストに関係ないファイルが編集された場合に起動させる必要がないので指定しておきました。

  • 多重起動抑止
8-14行目
jobs:
  cleanup-runs:
    runs-on: ubuntu-latest
    steps:
      - uses: rokroskar/workflow-run-cleanup-action@v0.2.2
        env:
          GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

jobs からはワークフローの定義になります。
cleanup-runs というJobが定義されていますが、 rokroskar/workflow-run-cleanup-action というアクションを呼び出しています。
これはPR作成後に連続的にgit pushした場合に前回起動したワークフローが稼働中だったら止める為に呼び出しています。
GitHub Actionsはプランごとに月あたりに利用できるジョブ実行時間上限が決まっているので、無駄なジョブ実行を避けたいです。
CircleCIとかだと標準的に止める機能があったりしますが、GitHub Actionsでは自分で制御してあげる必要があります。
以降の解説でも要所要所で出てきますが、 ${{ secrets.GITHUB_TOKEN }} というsecretsという箇所について説明します。
これはリポジトリの Settings > Secrets からGitHub Actionsから参照できるパラメータを登録したものを参照しているものです。クレデンシャル情報などはワークフローのyamlに直接書くのではなくSecrets登録して使います。 secrets. 以降のキーで値を登録してください。
但し GITHUB_TOKENは何も登録しなくても最初から参照できます。外部のCIとかだと別途登録しないとGitHubと連携できなかったりしますが、ここらへんは流石にGitHub標準ということでやりやすいですね。
ただ注意事項として標準で払い出されるtokenのアクセス可能な範囲はそのリポジトリのみに限定されます。
他のリポジトリへアクセス[3]する場合は別途tokenを用意してSecrets登録が必要です。

  • チェックアウト
16-23行目
  test-with-code-coverage-job:
    name: code coverage job.
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 128

test-with-code-coverage-job がテストとカバレッジ取得のジョブ本体になります。
actions/checkoutfetch-depth を指定していますが、後続処理でgitログを参照してソースの差分を取得する為[4]に必要です。PRのコミット履歴が大量になる場合は調整してください。

  • アプリケーションセットアップ
24-41行目
      - name: Setup Node.js
        uses: actions/setup-node@v1
        with:
          node-version: ${{ secrets.NODE_VERSION }}
      - name: Cache node modules
        uses: actions/cache@v2
        env:
          cache-name: cache-node-modules
        with:
          path: ~/.npm
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-
      - name: Install Dependencies
        run: npm ci

テストを動かせる状態にする為のステップです。
アプリケーション動作環境自体やパッケージのインストールを行います。
テスト動作用で環境変数等の調整が必要な場合も適宜行ってください。
パッケージのインストールは毎回行われますので、キャッシュを使って高速化しましょう。
ここらへんは、GitHub ActionsをCIとして使う際の紹介記事がいっぱいありますのでいい感じにしてください。

  • テスト&カバレッジ取得
42-54行目
      - name: jest test
        run: |
          mkdir -p coverage
          npm test -- --collect-coverage --verbose | \
            tee | \
            sed -E "s/"$'\E'"\[([0-9]{1,2}(;[0-9]{1,2})*)?m//g" | \
            grep "Coverage summary" -A4 | \
            tail -n 4 > coverage/coverage-summary.log
          status=("${PIPESTATUS[@]}")
          if [ ${status[0]} -ne 0 ]; then
            echo "jest faild."
            exit 1;
          fi

こちらがテストとカバレッジ取得本体です。
GitHub Actionsはステップをシェルスクリプトで記述できるのでゴリゴリ書いています。
npm testでjestを実行してカバレッジ取得と途中経過が追いやすいようにオプション指定しています。
jestの出力をgrepしてレポートのサマリー部分をファイル出力しているのですが、出力内容にカラーシーケンスが含まれているのでsedで削除しています。
jest版ではカバレッジ取得とテストを同時に行う様にしています。jest内でFAILが発生した場合にワークフローを失敗状態にする為にpipeステータスを判定しています。スクリプト内で exit 1 するとワークフロー自体がエラー扱いになります。
カバレッジのみ取得するのであれば、ステータス判定は不要だと思います[5]

  • カバレッジレポートアップロード
55-74行目
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
      - name: Upload coverage html
        id: upload-s3
        run: |
          PR_NUM=$(jq -r '.number' $GITHUB_EVENT_PATH)
          REPOSITORY_NAME=$(jq -r '.repository.name' $GITHUB_EVENT_PATH)
          aws s3 sync coverage/lcov-report/ s3://${{ secrets.AWS_S3_BUCKET }}/coverage-report/$REPOSITORY_NAME/$PR_NUM/ --delete
          aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --paths '/coverage-report/$REPOSITORY_NAME/$PR_NUM/*'
          echo "* [レポート全体](https://${{ secrets.AWS_S3_WEB_HOST }}/coverage-report/$REPOSITORY_NAME/$PR_NUM/index.html)" | tee -a coverage/coverage-report-urls
          HEAD_SHA=$(jq -r '.pull_request.head.sha' $GITHUB_EVENT_PATH)
          BASE_SHA=$(jq -r '.pull_request.base.sha' $GITHUB_EVENT_PATH)
          git diff --name-only --diff-filter=ACMR ${BASE_SHA}..${HEAD_SHA} -- '**/*.js' '**/*.vue' | \
            grep -v "__tests__/" | \
            xargs -I{} echo "* [{}](https://${{ secrets.AWS_S3_WEB_HOST }}/coverage-report/$REPOSITORY_NAME/$PR_NUM/{}.html)" | \
            tee -a coverage/coverage-report-urls

カバレッジレポートを参照できる場所にアップロードします。
GitHub Actionsで成果物を扱うには actions/upload-artifact アクションで保存できるのですが、圧縮形式になってそのままレポートを参照できないので使い勝手がよくありませんでした。
今回はAWSのWebホスティングしたS3にアップロードしました。
S3にアップロードするだけでリンクからすぐに参照できますし、ライフサイクルポリシーを設定することでアップロードされた古いレポートを自動で削除できるので要件としては大変マッチしています。

credentials設定は前述したとおりSecretsに登録しておきましょう。
aws-actions/configure-aws-credentials アクション実行後はあとは普通にAWS CLIが使える様になります。
今回はS3にsyncコマンドでアップロードしています。
あとWebホスティングしたS3の前段にCloudFrontを配置していたのでキャッシュの制御をaws cliで行っています。

スクリプト内で $GITHUB_EVENT_PATH という環境変数を参照していますが、ワークフローのイベント情報がJSONファイルで渡ってくるのでjqコマンド色々情報を取得できます。
実際にPRの番号だったり、リポジトリの名称を取得してS3のアップロードパスの生成に利用しています。
こうすることで他のリポジトリでも一つのS3バケットで管理できますし、色々なリポジトリにワークフローを手直しせずに使い回せるので使わない手はないです。
あと同様に HEAD_SHABASE_SHA のくだりではPRのベースブランチのコミットIDとPRの最終コミットIDを取得して git diff して差分ファイルの一覧を取得しています。
差分ファイル一覧からgrepでテストファイルを除外[6]してxargsを組み合わせでWebホスティングされているS3のURLに変換してファイル出力させています。

  • PRコメントする
75-97行目
      - name: Read coverage summary
        id: coverage-summary
        uses: juliangruber/read-file-action@v1.0.0
        with:
          path: coverage/coverage-summary.log
      - name: Read coverage report urls
        id: coverage-report-urls
        uses: juliangruber/read-file-action@v1.0.0
        with:
          path: coverage/coverage-report-urls
      - name: Covarage summary comment
        uses: marocchino/sticky-pull-request-comment@v1
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          header: coverage-summary
          message: |
            ## Coverage summary
            ${{ steps.coverage-summary.outputs.content }}
            ## Coverage report
            ${{ steps.coverage-report-urls.outputs.content }}

テスト&カバレッジ取得でカバレッジのサマリーと、カバレッジレポートアップロードでの差分ファイルのカバレッジレポートのURL一覧がそれぞれファイル出力されているのでそれを読み込んでPRコメントをしています。
ファイルの読み込みは juliangruber/read-file-action を利用しています。
アクション実行時のidでstepsコンテキストから参照できるようになります。
PRのコメントは marocchino/sticky-pull-request-comment を利用しています。
再プッシュ時に同一コメントとして行う場合には header を指定します。

GitHub Actionsを使ってみた所感など

今回紹介したワークフロー以外でも実際の業務でいつくかのワークフローを作ってみての感想になります。

  • 公開アクションをうまく組み合わせることでかなり楽になる
    今回のワークフローもいくつかのサードパーティの公開アクションを利用しましたが大変便利です。
    ワークフローの記述量がかなりコンパクトになります。
    Slack連携とかもサクッとできます。
    公開アクションを探したり、比較して導入するなど結構楽しいです。
  • ワークフローのトリガーを細かく制御できる
    今回では特定ファイルが更新された時のみに動く様にしていますが、ここらへんはさすが後発という感じです。
    cron tabも使えるので、定期パッケージアップデートのPR作成とかに使っています。
    手動実行でパラメータ指定ができるのも大変便利です。
  • GitHubに統合されているという利点
    外部サービスだと別サイトに遷移しないといけないことも多いですが、さっと確認できるのがいい。
    あとアクセストークンが勝手に払い出されて便利なのと、外部サービスに書き込み権限をもったトークンを預けなくてよいという安心感があります。
    GitHub APIを組み合わせて色々やっていこうかなーという気にさせます。

最後に

いかがでしたでしょうか?
簡易的な内容ではありますが、PR作成時に常にカバレッジが見れる状況にはなりました。
欲を言えば、前回のカバレッジとの差分の変動も見れるといいのですが、コミットログとレポートを紐付けて管理して差分抽出する仕組みがほしそうです。
そこまでやるなら、素直にCodeCovを使ったほうが良さそうです。
幸いなことにCodeCovにGitHub Actionsの公開アクションが用意されているみたいなので、今回のワークフローを少し手直しするだけで使えそうです。

現状はプロダクトコードを変更した箇所のみ、カバレッジを落とさずにやっていければい良いので満足できています。
なにより必ず目に入るPRのコメントとして可視化されるので、意識向上に繋がっていると実感できています。

明日はチームメンバーの @yknoguchi が投稿してくれるみたいなので、よかったら覗いてあげてください。


追記:
投稿されました

https://qiita.com/yknoguchi/items/2dc2888c5e3853271534

参考URL

脚注
  1. PHPUnitはLaravelを意識したつもり ↩︎

  2. テストコードの配置 ↩︎

  3. 例えば、他のプライベートリポジトリをsubmoduleにしている際のcheckoutや、別リポジトリへのPR作成等 ↩︎

  4. 標準では最新の履歴だけがチェックアウトされます ↩︎

  5. PHPUnit版は別途ECRイメージを使ったテストが存在していたので、カバレッジ単体としてワークフローを定義していてテスト結果の判定処理は含めていません ↩︎

  6. __tests__ ディレクトリがテスト置き場です。 ↩︎

Discussion

ログインするとコメントできます