🌊

【Webフロントエンド】テスト自動化のロードマップを考えてみる

2025/02/03に公開

はじめに

チーム内に有識者がいる場合は、まず模範となるコードを書いてもらい、そのコードを基に他のメンバーが学びながら進めるのが理想です。一方、初心者だけの環境では、良いテストと悪いテストの区別がつかず、テストの目的も不明瞭なままテストを書き始めるため、モチベーションが維持できず、結果として技術的負債が増加する傾向があります。そこで、まずはチーム全体でテストの基本を学び、必要最低限のテストプラクティスとその目的を共有することが重要だと考えます。

Level 100: 準備

テストを知る

Webフロントエンドのテストについて体系的に学ぶには、まず書籍『フロントエンド開発のためのテスト入門』を読みましょう。こちらの書籍の紹介記事「フロントエンドのテスト手法、何がある? 知っておきたいテストとその戦略を網羅的に解説」を先に読んでも良いでしょう。

次に、余力があれば書籍『Jestではじめるテスト入門』もおすすめです。読み物としても楽しめる一冊です。

これらの書籍や記事を通じて、テストの外観をざっくりと把握することを目指します。

静的解析

TypeScript

TypeScript は JavaScript の上位互換なので、特別な理由がない限り積極的に導入しましょう。TypeScriptを学ぶには、書籍『プロを目指す人のためのTypeScript入門』がおすすめです。

また、OpenAPIやSwaggerを利用している場合、スキーマ定義をSingle Source of Truthとして扱うことで、データの一貫性を確保できます。スキーマ定義を基にTypeScriptの型を自動生成したり、生成AIを活用してテストデータを作成する際の指示に活用することも可能です。フロントエンドで独自に型を作成するのは避け、スキーマ定義を基準とすることが推奨されます。

ESLint、Prettier

Linter, Formatterは、husky, lint-staged, eslint, prettier, eslint-config-prettier の組み合わせで、コミットの直前に実行するのが一般的と思われます。

生成AIについて

全てのテストを自前で書くのは工数がかかりすぎるため、生成AIが不可欠です。一方で、生成AIの使用によりカバレッジは向上したものの、メンテナンスできない技術的負債が増えたという話も聞きます。これを回避するには、適切なプロンプトエンジニアリングを行い、生成AIの提案を全て採用するのではなく価値のあるテストのみを残す必要があります。

プロンプトエンジニアリングは、OpenAIAnthropic のドキュメントを基本としつつ、Prompt Engineering Guide なども参考に勉強してください。また、各プロジェクトの技術スタックやPJ独自のコンテキストをまとめて、copilot-instructions.md.cursorrules、その他のカスタム指示などに記載しておくと、より的確なテストを作成してくれるようになります。チームでエディタを統一することは難しいものの、この設定自体はチームで共有すると良いでしょう。

この業界は変化が激しいので、おすすめのAIモデルには言及しません。常に最新情報をキャッチアップするようにしましょう。
https://www.swebench.com/
https://lmarena.ai/?leaderboard

次のレベルに進む前に

Webフロントエンドに限らず、一般的なソフトウェアテストの手法について学ぶことも大切です。以下の書籍を参考にすることで、QAエンジニアやテストエンジニアなどとのコミュニケーションも円滑に進めやすくなるでしょう。

また、テスト分野における最新の知見を得るために、テスト駆動開発の第一人者である@t_wadaさんをフォローすることをおすすめします。彼の講演資料には参考になるものが多数あるため、ぜひチェックしてみてください。

Level 200: シンプルなテストを書く

単体テスト

書籍『単体テストの考え方/使い方』によると、単体テスト(Unit Test)とは以下の3点を全て満たすテストを指します。

  • 「単体(unit)」と呼ばれる少量のコードを検証すること
  • 実行時間が短い
  • 隔離された状態で実行される

ここで「単体」や「隔離」に対する解釈の違いから、単体テストは古典学派とロンドン学派の2つに分かれます。

単体の捉え方 隔離の考え方 モックの使用
古典学派 1単位の振る舞い テストケース同士を隔離 少なく抑える
ロンドン学派 1つのクラス クラスを外部依存から隔離 積極的に使用

ロンドン学派は依存から隔離され非常にシンプルですが、モックの使用が増えすぎるとモックの設定や管理が複雑化し、それが原因でテストの信頼性を損なう場合があります。また、実際のユースケースから乖離したテストになりやすいため、Webフロント開発においては、古典学派寄りのテスト戦略を採用するのが適切と考えます。

テスト・ダブル

上記の説明にも出てきた「モック」という言葉は誤解を招きやすいので、Gerard MeszarosのxUnit Patternsを元に、モック、スタブ、スパイの違いをまとめました。

狭義のモック(Mock) スタブ(Stub) スパイ(Spy)
目的 テスト対象から依存先への呼び出しや引数を検証する 依存先がテスト対象に対して固定値や決まった挙動を提供する テスト対象の関数が呼び出されたかどうかを記録する
主な使用場面 API呼び出しや外部システムへのインタラクションの検証 外部システムやリソースの代替として利用 関数の呼び出し回数や引数を確認する

xUnitではこの3つにダミー、フェイクを足し合わせてテスト・ダブルと呼びます。一方、Jestでは「モック」という用語をテスト・ダブル全般の意味(広義のモック)で使用しているため、名前と機能の関連性に注意してください。例えば、spyOn はxUnitのスパイの機能を持ちますが、それ以外の機能も持っています。

リファクタリング耐性

テストにおいては、速度、保守性、再現性などのバランスが重要であり、何が最も大切かを一概に言うことは難しいです。しかし、その中でも欠かせない要素としてリファクタリング耐性が挙げられます。リファクタリング耐性とは、コードをリファクタリングしてもテストが失敗しにくい特性のことを指します。

例えば、次のような関数があるとします。

  • 関数A は、内部で 関数B と 関数C を実行する。
  • リファクタリングで 関数B を 関数B1 と 関数B2 に分割した。

このとき、関数Aのテストへの影響は次のようになります。

  • 関数Aの結果を検証するテスト
    → 成功(リファクタリング耐性がある)
  • 関数Bが呼ばれたこと(内部構造)を検証するテスト
    → 関数Bはもう存在しないので失敗(リファクタリング耐性がない)

このように、振る舞いが正しくてもテストが失敗する現象を偽陽性(誤検知)と呼びます。偽陽性が増えると、テストの信頼性が下がるため、テストはリファクタリングに強い設計にすることが重要です。

リファクタリングについて理解を深めたい場合は、書籍『リファクタリング』がおすすめです。

コードカバレッジ

カバレッジレポートの読み方

vitest run --coverage でカバレッジレポートが出力されます。

緑色のセルは十分なテストが書かれているという意味です。緑色になっていないセルを見つけたら、横に表示されているUncovered Lineを参考にテスト漏れを確認しましょう。

open coverage/index.html で詳細なレポートも見られます。全体のカバレッジやファイルの各行を何回通っているかなどの詳細情報を知りたい時に確認してください。

4種類のカバレッジ(ステートメント、ブランチ、関数、行)は、C0/C1/C2/MCCカバレッジとの対応を考えると、ブランチが C1、それ以外が C0 に近いです。一番カバレッジレベルが高いのは MCC ですが、Vitestに該当する指標はありません。カバレッジレベルの高さからも分かりますが、プログラムのバグの多くは分岐で発生するため、最低でもブランチカバレッジを実行すべきという意見が大勢です。

カバレッジの使い方

カバレッジは、テストの不足を把握するための有効な指標であり、高いに越したことはありません。しかし、無理にカバレッジを上げようとすると、プロダクションコードの価値を高めない、いわゆるカバレッジ稼ぎの負債コードが増える恐れがあります。一方で、カバレッジが低すぎる場合は、明らかにテストが不足しているサインといえます。記事「Code Coverage Best Practices」では、カバレッジ60%を許容範囲としています。これを下回る場合は、テストの見直しを行う指標として活用できるでしょう。

新規プロジェクトにおいては、カバレッジ目標を設定し、ある程度の強制力を持たせることも有効です。特に早期に導入することで、テスト文化をチームに定着させやすくなります。その際、coverage.thresholds を利用して、カバレッジが設定した閾値より低い場合はテストを失敗させる仕組みを導入できます。ただし、いきなり100%を目指すのではなく、80%程度の現実的な範囲を目標とするのが望ましいでしょう(記事「Measuring Coverage at Google」によると、Googleのプロジェクトにおけるカバレッジの中央値は78%だそうです)。その際、カバレッジを上げることが目的化していないかを常に確認し、負債コードが増えていないか注意が必要です。もし負債コードが増えている場合は、カバレッジの強制をやめ、テストの質に重点を置く方向へ切り替えるべきです。

以下は GitHub Actions で利用する yml のサンプルです。カバレッジ目標を設定し、自動テストを行い、アーティファクトにカバレッジレポートを保存します。

.github/workflows/vitest.yml
vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      include: ['src/**/*.tsx'],
      reporter: ['text', 'json', 'json-summary', 'html'],
      thresholds: {
        lines: 80,
        statements: 80,
        functions: 80,
        branches: 80,
      }
    }
  },
})
.github/workflows/vitest.yml
name: Vitest Tests

on:
  pull_request:
    branches: [ main ]
    paths:
      - 'src/**'
      - 'vitest.config.*'
      - 'package.json'
      - 'package-lock.json'

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests with coverage
        run: npx vitest run --coverage
        
      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage
          retention-days: 14

      # カバレッジをPRにコメントとして投稿
      - name: Comment coverage report
        if: github.event_name == 'pull_request'
        run: |
          COVERAGE=$(cat coverage/coverage-summary.json | jq -r '.total.branches.pct')
          echo "Branch coverage: $COVERAGE%"
          gh pr comment ${{ github.event.pull_request.number }} -b "📊 Branch coverage: $COVERAGE%"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

スナップショットテスト

スナップショットテストは、コンポーネントの出力(DOMツリーやJSONなど)を記録し、意図しない変更を検出する回帰テストの手法です。主にUIのリグレッションテストに用いられ、コンポーネントの構造が変更された際に警告を出します。後述するStorybookを使ったVRTよりも手軽に実施することができます。

Vitestでは toMatchSnapshot を使用してスナップショットを生成し、テスト時に比較を行います。出力に変更がある場合、テストは失敗しますが、変更が意図したものであれば、vitest -u でスナップショットを更新します。

https://vitest.dev/guide/snapshot

Storyの作成

コンポーネントの確認

Storybook の主な利点の一つは、リアルタイムでコンポーネントの見た目を確認できることです。Storybook の Controls タブを使うことで、コンポーネントに渡される Props を変更し、その変化を視覚的に確認できます。

Storybook は主に UI の確認に使用されるため、Storybookでコンポーネントが描画される際に API が呼び出されるのは避けた方が良いでしょう。API をモックする方法もありますが、API 処理を上位のコンポーネントに任せ、結果を Props で受け取る形にすることで、API からコンポーネントを切り離すことができます。この方法が推奨されます。

UIカタログ

UI カタログとして活用することで、自分以外のエンジニアが作成したコンポーネントを再利用できるかどうかの判断を迅速に行えるほか、デザイナーや PM と共有することで、意思疎通が円滑になり、全体の工数削減にもつながる可能性があります。Storybookをデプロイするには Chromatic が便利です。Chromatic は VRT も実施できます。
https://storybook.js.org/tutorials/intro-to-storybook/react/en/deploy/

また、企業のブランディングとして活用することもできます:

次のレベルに進む前に

ここまでの内容と 関連記事:『【Webフロントエンド】テスト自動化のガイドラインを考えてみる』の カスタムフックテスト までを理解できていればプロジェクトに自動テストを導入し始めても問題なさそうです。次回の工数見積りにテストの項目を追加してみましょう。

Level 300: 多様なテストを書く

結合テスト

結合テスト(Integration Test)とは、上記の 「単体テスト」 の3つの定義の内1つでも欠いたテスト。主にテストコードの中で複数のモジュールを読み込み、連携に重きを置いたテストです。主要なハッピーパスや、モジュール間連携で初めて表面化する異常系をテストすることを目的とします。

React Testing Library の開発者が提唱する Testing Trophy において、最も多く書くべきとされるのが結合テストです。結合テストは、単体テストよりも実際のユースケースに近い形で実行されるため信頼性が高く、少ないテストケースで広範囲のコードをテスト可能という利点があります。このため、単体テストで細かい確認を行うよりも、結合テストで全体の確認を行ったほうが効率的な場合もあります。

また、テストの順番に関して、一般的にテストは単体テストから始めるべきと考えがちですが、既存コードのリファクタリングにおいては、全体の振る舞いを保ちつつ作業を進められるため、結合テストから書き始めることで安心感を持って作業を進められるという考え方もあります。

MSW

MSW (Mock Service Worker)は、Service Worker を利用したAPIモックツールです。

以下のような目的がある場合、MSWを選ぶのが適切です。

  • 異常系のテストをしたい
  • 動的レスポンスが欲しい
  • fetchに到達する前にモックするのではなく、fetchを含むネットワークレイヤーでモックしたい(Chrome DevToolsからAPIリクエストがインターセプトされているのを確認できる)
  • Storybookでもリクエストハンドラーを使い回したい

これらを踏まえて、フレーキーテストを回避しながらも、モックを最小限に抑えて実際の挙動に近づけることができるため、APIモックにはMSWを全面的に採用する方針で問題ないと考えます。

Storybook Play function

Play function は Storybook 上のシナリオベースのインタラクションテストを実行する機能です。Vitest で Testing Library を使ったときとほぼ同じテストが実行できるのですが、どちらか一方を書けば十分です。

また、@storybook/test-runner を導入して、Storyオブジェクトのレンダリングと Play function を自動テストすることもできます。Playwrightをインストールし、Storybookをビルドした上でテストランナーを実行するという流れです。以下は GitHub Actions で利用する yml のサンプルです。

.github/workflows/storybook.yml
package.json
{
  "scripts": {
    "storybook:build": "storybook build -o storybook-static",
    "test-storybook": "test-storybook --maxWorkers=2"
  }
}
.github/workflows/storybook.yml
name: Storybook Tests

on:
  pull_request:
    branches: [ main ]
    paths:
      - 'src/**'
      - '.storybook/**'
      - 'stories/**'
      - 'package.json'
      - 'package-lock.json'

jobs:
  storybook-test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci

      - name: Install Playwright
        run: npx playwright install --with-deps

      - name: Build Storybook
        run: npm run storybook:build
        
      - name: Serve Storybook and run tests
        run: |
          npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
            "npx http-server storybook-static --port 6006 --silent" \
            "npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook"

https://storybook.js.org/docs/writing-tests/test-runner#run-against-non-deployed-storybooks

ビジュアルリグレッションテスト

ビジュアルリグレッションテスト(Visual Regression Test, VRT)は、コンポーネントのスタイルが意図せず変更されていないかを検証するテストです。単体テストだけでは、グローバルCSSや他のコンポーネントからの予期せぬスタイルの影響を検知することが難しいため、Storybookなどを使用してコンポーネントの見た目を画像として保存し、変更前後で比較することで検証を行います。VRTはレスポンシブデザインを採用していたり、不要なCSSの炙り出しに苦労していたりする場合には特に有効です。

Storybookを使用している場合、Chromaticを導入する方法が手軽ですが、ここでは自前のCIで実行する方法について説明します。具体的には、Storybookをビルドした後に、storycap を使用してスクリーンショットを生成し、reg-suit を使用して新旧画像を比較します。検証結果のレポートは、S3などのクラウドストレージに保存できます。以下は GitHub Actions で利用する yml のサンプルです。

.github/workflows/vrt.yml
regconfig.json
{
  "core": {
    "workingDir": ".reg",
    "actualDir": "__screenshots__",
    "thresholdPixel": 50,
  },
  "plugins": {
    "reg-publish-s3-plugin": {
      "bucketName": "your-bucket-name"
    },
    "reg-notify-github-plugin": {
      "clientId": "reg-publish-github-plugin"
    }
  }
}
package.json
{
  "scripts": {
    "storybook:build": "storybook build -o storybook-static",
    "vrt:snapshot": "storycap --serverCmd \"npx http-server storybook-static -p 9001\" http://localhost:9001",
    "vrt:run": "reg-suit run",
  }
}
.github/workflows/vrt.yml
name: Visual Regression Test

on:
  pull_request:
    branches: [ main ]
    paths:
      - 'src/components/**'
      - 'src/styles/**'
      - '.storybook/**'

jobs:
  visual-regression-test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build Storybook
        run: npm run storybook:build
      
      - name: Generate snapshots
        run: npm run vrt:snapshot
      
      - name: Setup AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        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: Run visual regression test
        run: npm run vrt:run
        env:
          REG_NOTIFY_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload VRT results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: vrt-results
          path: .reg

Webアクセシビリティ

Webアクセシビリティとは、障害など何らかの制約のある人でもWebコンテンツにアクセスできることを意味します。この重要性について深く考えたことがない人も多いかもしれません。興味を持った方は、書籍『Webアプリケーションアクセシビリティ』をぜひ読んでみてください。

testing-library

testing-libraryのクエリ検索の優先順位 に従って、getByRoleを最優先で使用することで、自然とアクセシビリティを考慮したコードを書けるようになります。ロールに関しては、WAI-ARIA の基本 を参照してください。

eslint

eslint-plugin-jsx-a11y を導入しましょう。

Axe

Axeは、axe-coreを基盤としたWebアクセシビリティ向けのツールスイートです。

次のレベルに進む前に

ここまでは、優先的に取り組むべきテストについて解説してきました。ここからは、メンテナンスコストが高くなる可能性のあるテストや、自動テストの周辺技術について触れていきます。これらを適切に取り入れることで、より柔軟なテスト環境を構築することが可能になります。

Level 400: 応用

E2Eテスト

E2Eテスト(End-to-End Test)は、ほぼすべての依存を使用してエンドユーザー目線で行うテストです。一般的には単体テストや結合テストが一通り完成した後に実施されます。しかし、レガシープロジェクトでは、仕様が不明瞭で単体・結合テストを導入しにくいため、まずE2Eテストでシステム全体の動作を把握すべきだという意見も多くあります。

基本的には、単体テストや結合テストでカバーできる領域はそれらに委ね、E2EテストはE2Eでしか検証できない機能やシナリオベースのテストに注力すべきです。

E2Eテストが特に有効なケースとして、サーバーなどの外部依存を含めたテストに加え、以下のようなブラウザ固有の機能を使用するテストが挙げられます:

  • 複数画面をまたぐ機能
  • 画面サイズから算出するロジック
  • Cookieやローカルストレージ

Playwright

E2Eテストツールには、Selenium, Cypress, Puppeteerなどの選択肢もありますが、記事公開時点では最も支持されている Playwright を選ぶのが適切でしょう。レガシーブラウザのサポートなど特別な理由がある場合は、他のツールの検討も必要かもしれません。

https://npmtrends.com/cypress-vs-playwright-vs-puppeteer-vs-selenium-webdriver

レコーディング機能などを使い効率的にテストを作成したら、自動テストを行い、アーティファクトに playwright-report を保存します。以下は GitHub Actions で利用する yml のサンプルです。

.github/workflows/playwright.yml
playwright.config.ts
export default defineConfig({
  use: {
    trace: 'retain-on-failure',
    video: 'retain-on-failure',
  },
});
.github/workflows/playwright.yml
name: Playwright E2E Tests

on:
  pull_request:
    branches: [ main ]
    paths:
      - 'src/**'
      - 'e2e/**'
      - 'playwright.config.*'
      - 'package.json'
      - 'package-lock.json'

jobs:
  playwright-test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      
      - name: Run Playwright tests
        run: npx playwright test
        
      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

ダウンロードしたレポートを npx playwright show-report {解凍したフォルダ} で確認します。テストトレースや動画も記録しておくとデバッグに役立ちます。

このようなPlaywrightを使ったE2Eテストを学ぶには、書籍『[入門]Webフロントエンド E2E テスト』がおすすめです。

プロパティベーステスト

プロパティベーステスト(Property-based Testing)は、コードが満たすべき性質(プロパティ)を検証するテスト手法です。この手法では、入力値の範囲や制約を定義し、その範囲内で多数の値をランダムに生成して検証を行います。Vitestでは、fast-check を利用してプロパティベーステストを実行できます。

プロパティベーステストはランダムな値を生成するので、再現性はありません。一方で、今まで書いてきたテストのほぼ全てが再現性のあるテストであり、プロダクションコード完成後には回帰テストとして機能します。このように棲み分けられるので、適切に組み合わせることで、より堅牢なテスト戦略を構築できます。

ミューテーションテスト

ミューテーションテスト(Mutation Test)は、コードの一部を意図的に改変(ミューテート)し、その改変がテストで検出できるかを確認する手法です。つまりテストが実際にバグを検出できるかどうかを評価するために行われます。カバレッジが高いだけでは、テストの有効性を保証できません。ミューテーションテストは、カバレッジの「嘘」を見抜き、テストの信頼性を高める役割を果たします。Vitestでは、Stryker を使用することでミューテーションテストを実行できます。

テスト駆動開発(TDD)について

TDD(Test-Driven Development)は、まず失敗するテストを書き、そのテストを通過させる最小限のコードを実装し、最後にコードをリファクタリングすることで品質を高める開発手法です。書籍『テスト駆動開発』によると、その本質は「プログラミング中の不安をコントロールする手法」と書かれています。

この不安をどのようにコントロールするかについて、参考記事「テストの最小単位は不安」より引用します:

私は不安に対してテストを書いていますから、自分がこれから書くコードに対して自信を持っている、たとえばこれまで書いたことのあるコードによく似ているコードなのであれば、小さい単位のコードは書きません。

対照的に、これから書くコードに自分自身が不安を感じているようであれば、小さい単位のテストを書いてみます。小さい単位というのは、1メソッドに対するテストや、メソッドの中の一部分に対するテストです。

一方で次のようなスタイルもあります。

  • 新しいライブラリを使う時など、実際の動きに不安がある場合は、まず実装をして動きを確認。その後にテストを追加する。
  • 慣れている技術を使う時など、実際の動きに不安がない場合は、単体テストやStoryオブジェクトを使用したTDDを行う。

不安だから、小さいテストを先に書くのか、実装を先に書くのか。それぞれのスタイルはあると思いますが、個人的にはWebフロント開発でも十分に活用できる手法と感じています。

CI/CDの改善

実行時間の目標

実行時間の理想は5分以内、遅くとも10分以内を目指しましょう。E2Eテストに力を入れたとしても15分以内に収めたいです。

ワークフローの並列実行制御

PR に複数回コミットがプッシュされた場合、古い実行中のワークフローを中断し、最新のコミットに対するワークフローのみを実行する設定を行います。
PR の場合、group を {ワークフロー名}-refs/pull/{PR番号}/merge のように設定し、他の PR には干渉しないようにします。

タイムアウト設定

予期せぬ無限ループや処理待ちが発生すると、ワークフローが最長6時間動き続ける可能性があります。そのため、タイムアウトを設定します。
https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes

デフォルトシェル設定

デフォルトシェルに bash を設定すると、以下の形式で出力されます:

shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}

この設定により、パイプラインのエラーを拾うことで、エラー発生箇所が明確になります。

ジョブの並列実行

依存関係のないタスクを並列実行することで、全体の処理時間を短縮します。ジョブの依存関係は needs を使用して明示します。ここでは、matrix を使ったシャーディングまでは考えないこととします。

キャッシュ

package-lock.json に変更がない場合、node_modules ディレクトリのキャッシュを再利用することで、インストール時間を大幅に削減できます。

ライブラリ依存関係のバージョンアップ

DependabotのPRをトリガーとして、回帰テストをパスし、かつマイナーアップデートの場合に限り、自動マージを許可するといった運用が考えられます。
https://docs.github.com/ja/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions

デプロイ

GitHub Actions の yml に設定するか、GitHub Webhook で外部サービスにイベント通知するかといった方法になると思いますが、テストの範囲から外れるのでここでは触れません。

以上を GitHub Actions の yml に纏めるとこのようになります。

.github/workflows/nextjs.yml
.github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    target-branch: "develop"
.github/workflows/nextjs.yml
name: CI for Next.js Application

on:
  pull_request:
    types: [opened, synchronize]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash

jobs:
  install:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - name: Cache node_modules
        uses: actions/cache@v4
        id: node-modules-cache
        with:
          path: node_modules
          key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
      - name: Install Dependencies
        if: steps.node-modules-cache.outputs.cache-hit != 'true'
        run: npm ci

  lint:
    needs: install
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Restore node_modules
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
      - name: Run Lint
        run: npm run lint

  test:
    needs: install
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Restore node_modules
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
      - name: Run Tests
        run: npm test

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Restore node_modules
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
      - name: Build
        run: npm run build

  auto-merge:
    needs: [build]
    runs-on: ubuntu-latest
    if: github.event.pull_request.user.login == 'dependabot[bot]'
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v2
        with:
          alert-lookup: true
          compat-lookup: true
          github-token: "${{ secrets.GITHUB_TOKEN }}"
          
      - name: Enable auto-merge for Dependabot PRs
        if: |
          steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
          steps.metadata.outputs.update-type == 'version-update:semver-patch'
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CI/CDでおすすめの書籍は以下です

監視

監視は自動テストの補完的な役割を果たし、テストでは検出しきれなかった問題や本番環境特有の課題をカバーするために重要です。

Datadog、New Relic、Sentry などの SaaS ツールを活用して行いますが、詳細についてはここでは触れません。

監視でおすすめの書籍は以下です

Discussion