11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub Actionsを使ってPR時にJestをいい感じに実行したい

Last updated at Posted at 2023-12-15

この記事は ZOZO Advent Calendar 2023 シリーズ 9 の 16 日目の記事です。

📍はじめに

GitHub Actions を使って Jest を実行するワークフローを作成したので、その際に検証したこと(オプションなど)を備忘録も兼ねて書いていきます。

1. やりたいこと

  • PR をオープンした時にテストを実行したい
  • なるべく実行時間は短縮したい

2. とりあえず結論

  • npm と Jest をキャッシュする
  • テストが1つでも失敗したら中断する
  • 実行環境の CPU を元に Jest のワーカー数を指定する

3. 試したこと

3-1. --bail

テストが何回失敗した場合にテストを中止させるかを指定します。デフォルトの回数は 1 です。


▼ 結果

こちらのオプションは実行時間には関係なさそうですが、テストが通らなかった場合にワークフローを中断したいので追加しました。

採用🙆

3-2. --forceExit

全てのテストの実行が終了した後に Jest を強制終了させるオプションです。
テストコードによって設定されたリソースが十分にクリーンアップできない場合に便利ですが、各テスト内でクリーンアップできていることが望ましいです。


▼ 結果

こちらも単体で実行時間を短縮するものではないですが、元々ローカルで使用しているテストコマンドで指定しているためそのまま採用しています。

採用🙆

3-3. --runInBand

Jest はデフォルトで並列実行しますが、このオプションを付けることで直列実行できます。


▼ 結果

CI 上では試していないのですが、ローカルで比較した所むしろ 40s 程遅くなってしまいました。

不採用🙅

3-4. --max-workers

Jest 公式ドキュメントのトラブルシューティングに記載がありました。
CPU の数を検出し Jest に渡して並行処理可能数を指定します。
GitHub Actions を使っているので、github-actions-cpu-cores を使って CPU の数を検出できます。


▼ 書き方

- name: Get number of CPU cores
  id: cpu-cores
  uses: SimenB/github-actions-cpu-cores@v2
  
- name: Run test
  run: yarn jest --max-workers ${{ steps.cpu-cores.outputs.count }}

CI 上で実行すると ${{ steps.cpu-cores.outputs.count }}2 になりました。


▼ 結果

このオプションを指定しない場合と比較して 40s 程速くなりました🎉

採用🙆

3-5. --onlyChanged

変更があったファイルのみテストを実行します。


▼ 結果

なぜか CI 上で変更が検知されず実行されませんでした。
また、node のバージョンアップなど直接変更がない場合に発生した不具合を検知できないおそれがあるため見送りました。

不採用🙅

3-6. --changedSince

こちらも同様に変更差分に影響があるテストのみ実行します。差分を検知する対象のブランチやコミットハッシュの指定が可能です。


▼ 書き方

- name: Checkout
  uses: actions/checkout@v3
  with:
    fetch-depth: "0"
# ...
- name: Run test
  run: npx jest --changedSince origin/master

▼ 結果

--onlyChanged と同じ差分実行のリスクを考慮し見送りました。
また、テスト自体は変更した分だけ実行するため速いのですが、fetch-depth: "0" でのチェックアウトにだいぶ時間がかかり、全体の実行時間はオプションを使わない場合と比べて約 2 倍となりました。

不採用🙅

3-7. --shard

テストの実行数を分割して並列実行します。
マトリックスを使用することで列挙した変数ごとに job を実行することができます。

参考: ジョブにマトリックスを使用する - GitHub Docs


▼ 書き方

jobs:
  test:
    runs-on: ubuntu-20.04

    strategy:
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    # ...
    steps:
        # ...
      - name: Run test
        run: npx jest --shard=${{ matrix.shard }}        

上記の場合、job を 4 分割で実行することになります。


▼ 結果

今回の場合、特段実行時間は短縮されませんでした。
また、GitHub Actions はワークフローの合計時間ではなく job 単位で無料枠が消費され、それ以上は分毎に従量課金されるためなしとしました。

不採用🙅

3-8. Jest のキャッシュ

Jest のキャッシュ機能を CI 上でも使えるように設定します。


▼ 書き方

👇キャッシュの配置先を指定し、

jest.config.ts
const jestConfig: Config.InitialOptions = {
    ...
    cacheDirectory: 'node_modules/.cache/jest',
    ...
}
export default jestConfig

👇 key を元に復元して使用します。

- name: Cache Jest
  uses: actions/cache@v3
  env:
    cache-name: cache-jest
  with:
    path: app-frontend/assets/node_modules/.cache/jest
    key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('app-frontend/assets/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-build-${{ env.cache-name }}-

最初は path を node_modules/.cache/jest で指定していたのですが、キャッシュの保存時に「いや、そんなパスないが😅(意訳)」と注意されました。

Warning: Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.

今回導入したリポジトリは複数パッケージが存在しているため、 うまく path を特定できていなかったようです。ここの解消にどハマりしました。


▼ 結果

--max-workers + Jest キャッシュ利用で 2 回目以降の実行がさらに 10 ~ 30s 速くなりました🎉

採用🙆

3-9. node_modules のキャッシュ

node_modules をキャッシュして、実行時にキャッシュが存在する場合は npm install をスキップします。


▼ 書き方

- name: Cache Node Modules
  uses: actions/cache@v3
  id: node_modules_cache_id
  env:
    cache-name: cache-node-modules
  with:
    path: "app-frontend/assets/node_modules"
    key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}

- name: Install Dependencies
  if: ${{ steps.node_modules_cache_id.outputs.cache-hit != 'true' }}
  run: |
    npm install

if: ${{ steps.node_modules_cache_id.outputs.cache-hit != 'true' }} の部分でキャッシュが存在するかを判断し、ない場合に npm install を実行しています。


▼ 結果

--max-workers + Jest キャッシュにプラスして試したのですが 2 回目以降 npm install がスキップされる分さらに 20s 程速くなりました🎉

しかし、actions/cache のドキュメントには非推奨と記載がありました。

Note It is not recommended to cache node_modules, as it can break across Node versions and won't work with npm ci

.node-version のハッシュ値をキャッシュキーに含めることで、バージョンに変更があった場合はキャッシュを読まないようにすることも考えました。
しかし、リポジトリが巨大なため何かしらの問題があった時のリスクを考慮し断念しました。

採用🙆からの不採用🙅

3-10. npm のキャッシュ

actions/setup-node を使用して npm の依存関係をキャッシュします。

参考: 依存関係のキャッシングの例 - Node.js のビルドとテスト - GitHub Docs


▼ 書き方

👇 withcache: "npm" を追加します。

- name: Use Node.js
  uses: actions/setup-node@v3
  with:
    node-version-file: "app-frontend/assets/.node-version"
    cache: "npm"
    cache-dependency-path: "app-frontend/assets/package-lock.json"

▼ 結果

--max-workers + Jest キャッシュのみの場合と比べて 5 ~ 10s と若干ですが短縮できました。

採用🙆

4. 完成形

補足すると、master ブランチに向けた PR のタイミングで実行しているのと、リポジトリには BE のコードも存在するためワークフローの実行対象ファイルは絞り込んでいます。

name: FrontendTestAssets

on:
  workflow_dispatch:

  pull_request:
    branches:
      - master
    paths:
      - "app-frontend/assets/.node-version"
      - "app-frontend/assets/src/**/*.ts"
      - "app-frontend/assets/src/**/*.tsx"
      - "app-frontend/assets/package.json"
      - "app-frontend/assets/package-lock.json"
    types: [opened, reopened, synchronize, ready_for_review]

jobs:
  test:
    if: ${{ github.event.pull_request.draft == false }}
    runs-on: ubuntu-20.04

    defaults:
      run:
        working-directory: ./app-frontend/assets

    steps:
      - name: Checkout Commit
        uses: actions/checkout@v3

      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version-file: "app-frontend/assets/.node-version"
          cache: "npm"
          cache-dependency-path: "app-frontend/assets/package-lock.json"

      - name: Install Dependencies
        run: npm ci

      - name: Cache Jest
        uses: actions/cache@v3
        env:
          cache-name: cache-jest
        with:
          path: app-frontend/assets/node_modules/.cache/jest
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('app-frontend/assets/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-

      - name: Get number of CPU cores
        uses: SimenB/github-actions-cpu-cores@v1
        id: cpu-cores

      - name: Run test SP
        run: npx cross-env DEVICE=sp jest --bail --forceExit --max-workers ${{ steps.cpu-cores.outputs.count }}

      - name: Run test PC
        run: npx cross-env DEVICE=pc jest --bail --forceExit --max-workers ${{ steps.cpu-cores.outputs.count }}

📍おわりに

PR をトリガーとしたワークフローの実行のためキャッシュの恩恵を受けられるのは 2 回目以降の実行になるものの、最適化しつつテストを実行する構成にできたのではないでしょうか。
今回始めて導入したため実運用上での比較はまだできていませんが、引き続き速度改善については検討していきたいと思っています!

以下参考にさせて頂きました🌟

おしまい🎅

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?