GitHub Actionsでカバレッジを可視化する
モチベーション
今携わっているプロダクトは、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カバレッジ例
jestカバレッジ例
(フロントエンドは最近jestを導入したばかりなのでカバレッジ率はまだこれからですw)
見ていただいて分かる通りGitHub ActionsがPRコメントとしてカバレッジのサマリーとレポートのリンクを投稿しています。
プロダクトコードに追加・修正があった場合に、該当のソースに対応したカバレッジのリンクをつける様にしています。リンクから詳細レポートをそのまま確認できます。
再プッシュされたら再度ワークフローが動き、先にコメントされたものが更新されます。
ワークフロー定義
GitHub ActionsのワークフローのサンプルコードはGistにアップしているので、良かったら見てみてください。
コードの説明はDB・APIといった外部サービスに依存しない分シンプルなjest版をベースに説明したいと思います。
PHPUnit版はjestとやっていることは大幅に変わりはありません。
ワークフロー解説
- ワークフロー宣言
name: test with code coverage
on:
pull_request:
types: [opened, reopened, synchronize]
paths:
- '**.vue'
- '**.js'
name
はワークフロー名。
on
はどのタイミングでこのワークフローを実行するか指定する場所です。
今回はPRコメントを付けたいので push
ではなく pull_request
を指定する必要がありました。
types
と paths
で更に条件を絞ってワークフローを制御することができます。
特にテストに関係ないファイルが編集された場合に起動させる必要がないので指定しておきました。
- 多重起動抑止
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登録が必要です。
- チェックアウト
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/checkout
で fetch-depth
を指定していますが、後続処理でgitログを参照してソースの差分を取得する為[4]に必要です。PRのコミット履歴が大量になる場合は調整してください。
- アプリケーションセットアップ
- 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として使う際の紹介記事がいっぱいありますのでいい感じにしてください。
- テスト&カバレッジ取得
- 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]
- カバレッジレポートアップロード
- 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_SHA
と BASE_SHA
のくだりではPRのベースブランチのコミットIDとPRの最終コミットIDを取得して git diff
して差分ファイルの一覧を取得しています。
差分ファイル一覧からgrepでテストファイルを除外[6]してxargsを組み合わせでWebホスティングされているS3のURLに変換してファイル出力させています。
- PRコメントする
- 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 が投稿してくれるみたいなので、よかったら覗いてあげてください。
追記:
投稿されました
参考URL
- Node.js のビルドとテスト
- 依存関係をキャッシュしてワークフローのスピードを上げる
- GitHub Actions のコンテキストおよび式の構文
- 環境変数
-
Webhook events and payloads
GITHUB_EVENT_PATH
(完了したwebhookイベントペイロードのファイルのパス) のjsonの中身です。 -
Codecov
カバレッジの可視化するにあたり検討、比較対象にしたレポートツール - [Codecov GitHub Action](https://gith
- ub.com/codecov/codecov-action)
Codecovと連携するGitHub Action
カバレッジ取得する所までは同じ。
Discussion