Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

Jest v28 shard オプションを使い、CI でカバレッジを計測できるようにする

こんにちは、ラーニング・学習トレーニングチームの id:tkdn です。 今日は Jest shard オプションを使って CI でどうカバレッジ計測をしたか について書いていきます。

Jest v28 shard オプション

Jest v28 から shard オプションが入りました。このオプションでテスト実行を指定の数で分割することができます。

Jest's own test suite on CI went from about 10 minutes to 3 on Ubuntu, and on Windows from 20 minutes to 7.

公式のアナウンスでもあるとおりですが、Jest 自身の CI でのテストが 20 分から 7 分になったという速度の変わりようです。

私たちのチームではリモートという状況の中で TDD /モブプロを実践しており、コミットによりバトンをつないでいます。そのため CI のスピードアップは開発体験を良くするだけでなく、マクロな視点ではリリースのリードタイムを短くすることにもつながります。

shard オプション、これを使わない手はありません。

shard オプションの使い方

我々は GitHub Actions を使っているので以下のように設定ファイルを書き換えました。特に難しいところはありません。ワンラインの実行オプションについては Jest のドキュメントに記載があります

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}

node_modules のキャッシュなどほかのステップは省いていますが、最小構成であれば上記の設定で動きます。これで testジョブは 4 つ並列で動くことになります。

環境にもよりそうですが、我々の環境では約 500 のテストケースで 5 分弱かかっていたところ、shard で 4 つ並行させると半分の 2 分半に短縮できました。 めちゃくちゃインパクトがあるわけではないのですが、ごくまれにモブプロで全体通してテスト結果みたいねというときや、ちょっとした修正後のデプロイなど、待ち時間が短くてうれしいタイミングはいくらでもあります。

shard オプションで実行した後カバレッジをどうするか問題

ただし問題が出てきます。プロジェクトでは coverageThreshold を設定していたため Jest が機械的に分割したテストのサブセットではグローバルなカバレッジ閾値を満たすことができないという状況が発生しました(分割せずに実行した場合はもちろんカバレッジを満たします)。

並列テスト A のコードパスで通ったモジュールやコンポーネントが必ずしも並列テスト A でテストされている保証はないので、そうなるだろうという予想はなんとなくしていましたが…。

類似した Issue: [Bug]: shard option and global coverageThreshold config · Issue #12751 · facebook/jest

解決策:並列テストのカバレッジを別ジョブで統合する

この問題についてはチームメンバーが解決策を持ってきてくれました。

参考になる Issue: Expose istanbul/nyc's check-coverage functionality in jest · Issue #11581 · facebook/jest

まずは並列で実行したテストのカバレッジを個別で Artifact として持っておきます(Artifact 自身は次のジョブで使います)。

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}
+   - run: mv coverage/coverage-final.json coverage/${{ matrix.shard }}.json
+   - name: Upload Artifact
+     uses: actions/upload-artifact@v3
+     with:
+       name: tmp-coverage
+       path: ./coverage

次に新しいジョブで並列実行により得られた Artifact を集めて nyc を使ってマージし(A)、nyc を使ってカバレッジ計測し(B)、簡易的なスクリプトでカバレッジを満たしていないファイルなどを出力します(C)。

test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - uses: actions/checkout@v3
    - run: npx jest --coverage --maxWorkers=1 --shard=${{ matrix.shard }}/${{ strategy.job-total }}
    - run: mv coverage/coverage-final.json coverage/${{ matrix.shard }}.json
    - name: Upload Artifact
      uses: actions/upload-artifact@v3
      with:
        name: tmp-coverage
        path: ./coverage
+ report-coverage:
+   needs:
+     - test
+   runs-on: ubuntu-latest
+   steps:
+     - uses: actions/checkout@v3
+     - uses: actions/download-artifact@v3
+       with:
+         name: tmp-coverage
+         path: tmp-coverage
+     - name: Merge coverage
+       # (A)ここで並列で得られたカバレッジをマージする
+       run: npx nyc merge tmp-coverage/ coverage/coverage.json
+     - name: Check coverage
+       # (B)マージしたカバレッジ計測判定を行う
+       run: npx nyc check-coverage --branches 100 --functions 100 --lines 100 --statements 100 -t coverage/
+     - name: Report coverage
+       if: failure()
+       run: npx nyc report --reporter=text -t coverage/ > coverage/coverage-result
+     - name: List Low coverage files
+       if: failure()
+       # (C)CIでの出力のためにスクリプトを実行します
+       run: node .github/tools/extract-low-coverage.js

これで大方やりたいことは実施できました。

注意点

気にしなくてはいけないこととしては、GitHub Actions の従量課金は実行時間の合計が対象となることです。実行時間が半分になり嬉しい限りなのですが、2 分半 * 4 並列が実際の実行時間となる点はしっかり踏まえて、お使いのプラン内の無料枠を考慮しましょう。少なくとも最初から並列する数を多めに設定しすぎてクォータを食いつぶした! なんてことがないようにするとよいですね。

まとめ

Jest v28 から使える shard オプションとそれを利用したカバレッジ計測について書きました。参考記事として以下に挙げた URL で狂喜したのですが、shard で実行後のカバレッジ計測のプラクティスがなかったので今回筆を取らせていただきました。

shard オプションは嬉しいのですが、実際には Jest のカバレッジの仕組みでフォローできるようになると良いですね。

Classi では CI の速度が気になった際にカッとなってやってしまうエンジニアを歓迎しています!!

参考 URL

© 2020 Classi Corp.