フロントエンドテスト戦略の最適解:ユニットテストからE2Eまで徹底強化する方法

2024/01/21に公開

はじめに

ソフトウェア開発において、テストの自動化は品質を維持しながら開発スピードを向上させる重要な要素です。しかし、テストの導入や運用には「どこまでテストすべきか」「適切なツールは何か」「CI/CD での最適な実行方法は?」といった多くの課題がつきまといます。

本記事では、ユニットテスト、コンポーネントテスト、E2Eテスト、APIテストの自動化 など、現代のフロントエンド・バックエンド開発におけるテスト戦略を体系的に解説します。さらに、モックデータの整理・最適化、CI/CD でのテスト実行フローの最適化、テストカバレッジの可視化、フィーチャーフラグを考慮したテスト設計 まで幅広くカバーし、実践的なアプローチを紹介します。

テストの導入・強化を検討している方や、現状のテスト戦略をより効果的にしたいと考えている方にとって、本記事が有益なガイドとなれば幸いです。

ユニットテストの導入・強化(Jest / Vitest)

ユニットテストの重要性

ユニットテストは、関数やクラスといった小さな単位の動作を検証 するための重要な技術です。主なメリットは以下のとおりです。

  • バグの早期発見
    個々の機能をテストすることで、問題を迅速に特定可能。
  • リグレッション(回帰)テストの自動化
    コード変更時に、既存機能が正しく動作し続けることを保証。
  • コードのドキュメント化
    テストケースを読めば、関数やクラスの意図が明確になる。
  • 開発のスピードアップ
    手動テストの手間が減り、継続的インテグレーション(CI)との統合が容易に。

Jest を活用したユニットテスト

Jest とは、JavaScript/TypeScript の主要なテストフレームワークです。

  • 主な特徴
    • シンプルな API で直感的に書ける
    • スナップショットテストやモック機能が充実
    • TypeScript との相性が良い

Jest の導入(TypeScript プロジェクト向け)

npm install --save-dev jest ts-jest @types/jest

その後、Jest を TypeScript 用に設定するための jest.config.ts を作成します。

jest.config.ts
import { Config } from "jest";

const config: Config = {
  preset: "ts-jest",
  testEnvironment: "node",
};

export default config;

Jest によるユニットテスト

  1. 関数のユニットテスト
    例えば、math.ts に sum 関数を実装するとします。
utils/math.ts
export function sum(a: number, b: number): number {
  return a + b;
}

この sum 関数をテストするには、math.test.ts を作成します。

utils/math.test.ts
import { sum } from "./math";

test("sum correctly adds two numbers", () => {
  expect(sum(1, 2)).toBe(3);
});

Jest を実行するには、以下のコマンドを使用します。

npx jest
  1. クラスのユニットテスト
    クラスのテストも容易に行えます。例えば、Counter クラスを作成します。
utils/counter.ts
export class Counter {
  private count = 0;

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }

  getCount(): number {
    return this.count;
  }
}

このクラスのテストを作成します。

utils/counter.test.ts
import { Counter } from "./counter";

test("Counter increments and decrements correctly", () => {
  const counter = new Counter();

  counter.increment();
  expect(counter.getCount()).toBe(1);

  counter.decrement();
  expect(counter.getCount()).toBe(0);
});
  1. 非同期処理のテスト
    非同期関数の動作を確認するために、async/await を使ったテストも可能です。
utils/fetchData.ts
export async function fetchData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Hello World"), 1000);
  });
}
utils/fetchData.test.ts
import { fetchData } from "./fetchData";

test("fetchData returns expected value", async () => {
  const data = await fetchData();
  expect(data).toBe("Hello World");
});

Vitest を活用したユニットテスト

Vitest とは、Vite に最適化された 超高速なテストフレームワーク です。
Jest に似た API を持ち、TypeScript のサポートも優れています。

Vitest の導入(TypeScript プロジェクト向け)

Vitest を使用するには、以下のパッケージをインストールします。

npm install --save-dev vitest

設定ファイル vite.config.ts を作成します。

vite.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
  },
});

Vitest によるユニットテスト

Vitest では、Jest とほぼ同じ書き方でテストが書けます。

  1. 関数のテスト
utils/math.test.ts
import { describe, it, expect } from "vitest";
import { sum } from "./math";

describe("sum function", () => {
  it("correctly adds two numbers", () => {
    expect(sum(1, 2)).toBe(3);
  });
});
  1. クラスのテスト
utils/counter.test.ts
import { describe, it, expect } from "vitest";
import { Counter } from "./counter";

describe("Counter class", () => {
  it("increments and decrements correctly", () => {
    const counter = new Counter();

    counter.increment();
    expect(counter.getCount()).toBe(1);

    counter.decrement();
    expect(counter.getCount()).toBe(0);
  });
});
  1. 非同期処理のテスト
utils/fetchData.test.ts
import { describe, it, expect } from "vitest";
import { fetchData } from "./fetchData";

describe("fetchData function", () => {
  it("returns expected value", async () => {
    const data = await fetchData();
    expect(data).toBe("Hello World");
  });
});

Vitest を実行するには、以下のコマンドを使用します。

npx vitest

Jest vs Vitest 比較

特徴 Jest Vitest
実行速度 比較的遅い 非常に高速(Vite ベース)
API 互換性 標準的なテストフレームワーク Jest API にほぼ準拠
TypeScript サポート 良好 優秀(Vite の TS サポートを活用)
非同期テスト async/await に対応 async/await に対応
Vite との相性 直接のサポートなし 最適化されている

どちらを選ぶべきか?

Vite を使っているなら Vitest、そうでないなら Jest が基本的な選択肢になります。

  • Jest は、Vite を使っていないプロジェクトや、従来の Jest に慣れている環境に適している。
  • Vitest は、Vite ベースのプロジェクトや、高速なテストが求められる環境に適している。

Jestの導入手順についてのブログ記事もありますので合わせてご参照ください。

https://shinagawa-web.com/blogs/jest-unit-testing-introduction

コンポーネントテストの最適化(React Testing Library / Storybook)

React のコンポーネントテストを最適化する方法として、React Testing Library(RTL)とStorybookを活用する方法を詳しく解説します。

React Testing Library(RTL)を活用したコンポーネントテスト

React Testing Library(RTL)は、実際のユーザー操作を模倣することを重視したテストフレームワークです。
ユニットテストやインテグレーションテストで利用され、ユーザー視点のテストを実施できます。

RTL の導入

まずは、必要なパッケージをインストールします。

npm install --save-dev @testing-library/react @testing-library/jest-dom
  • @testing-library/react: React のコンポーネントをテストするためのコアライブラリ
  • @testing-library/jest-dom: toBeInTheDocument() などのカスタムマッチャーを提供

基本的なテストの書き方

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Button from "../components/Button";

test("Button click triggers event", async () => {
  const onClick = jest.fn();
  render(<Button onClick={onClick}>Click me</Button>);

  await userEvent.click(screen.getByText("Click me"));

  expect(onClick).toHaveBeenCalledTimes(1);
});
  • ポイント
    • render(<Button onClick={onClick}>Click me</Button>)
      • render() を使ってコンポーネントを仮想DOMにレンダリング
    • screen.getByText("Click me")
      • screen 経由で要素を取得
    • getByText() でボタンのテキストから要素を探す
      • await userEvent.click(...)
    • userEvent.click() でクリックイベントをシミュレート
      • await を付けることで、非同期処理を適切に待つ
    • expect(onClick).toHaveBeenCalledTimes(1);
      • jest.fn() を使って onClick の呼び出し回数を検証

よく使うクエリ

RTL ではアクセスビリティを重視したクエリを使用するのが推奨されています。

クエリ 用途
getByRole ボタンや見出しなどの役割(role)を持つ要素を取得 screen.getByRole('button', { name: 'Submit' })
getByLabelText <label> に関連付けられた要素を取得 screen.getByLabelText('Email')
getByText 指定したテキストを持つ要素を取得 screen.getByText('Hello, world!')
getByTestId data-testid 属性を持つ要素を取得 screen.getByTestId('custom-element')

例:ラベル付き入力フィールドのテスト

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import LoginForm from "../components/LoginForm";

test("入力フォームのテスト", async () => {
  render(<LoginForm />);
  
  const emailInput = screen.getByLabelText("Email");
  await userEvent.type(emailInput, "test@example.com");

  expect(emailInput).toHaveValue("test@example.com");
});

テストの最適化

  • beforeEach / afterEach で共通処理をまとめる
  • jest.spyOn() で関数の監視
  • waitFor を活用して非同期処理を適切に待つ

例)

DataComponentコンポーネントは、fetchData() を使ってデータを取得し、useEffect でレンダリング時にデータを取得して表示するシンプルなコンポーネントです。

import { useEffect, useState } from "react";
import { fetchData } from "../utils/api";

const DataComponent = () => {
  const [data, setData] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const loadData = async () => {
      try {
        const response = await fetchData();
        setData(response.data);
      } catch (err) {
        setError("Failed to load data");
      } finally {
        setLoading(false);
      }
    };

    loadData();
  }, []);

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>{error}</p>;
  }

  return <p>{data}</p>;
};

export default DataComponent;

動作の流れ

状態 表示されるテキスト
データ取得中 Loading...
成功時 Hello, World!"(API のレスポンスに依存)
エラー時 "Failed to load data

このコンポーネントのテストコードは下記になります。

import { render, screen, waitFor } from "@testing-library/react";
import { fetchData } from "../utils/api";
import DataComponent from "../components/DataComponent";

jest.mock("../utils/api"); // API をモック化

describe("DataComponent", () => {
  beforeEach(() => {
    jest.clearAllMocks(); // 各テスト前にモックをリセット
  });

  test("API からデータを取得して表示する", async () => {
    fetchData.mockResolvedValueOnce({ data: "Hello, World!" });

    render(<DataComponent />);

    // 初期状態では "Loading..." が表示される
    expect(screen.getByText("Loading...")).toBeInTheDocument();

    // 非同期処理が完了後に "Hello, World!" が表示されることを確認
    await waitFor(() => expect(screen.getByText("Hello, World!")).toBeInTheDocument());

    // "Loading..." は消えていることを確認
    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
  });

  test("API がエラーを返した場合にエラーメッセージを表示する", async () => {
    fetchData.mockRejectedValueOnce(new Error("Failed to fetch"));

    render(<DataComponent />);

    expect(screen.getByText("Loading...")).toBeInTheDocument();

    // 非同期処理が完了後に "Failed to load data" が表示されることを確認
    await waitFor(() => expect(screen.getByText("Failed to load data")).toBeInTheDocument());

    // "Loading..." は消えていることを確認
    expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
  });
});

ポイント

  • テストを describe でまとめました。
    • 関連するテストを整理し、スッキリさせました。
  • beforeEach で jest.clearAllMocks() を追加
    • 各テストの間でモックの状態をリセットし、テストの独立性を確保
  • エラーハンドリングのテストを追加
    • fetchData が失敗したときの挙動もテストすることで、より堅牢なコードに

Testing Library / Reactの導入手順についてのブログ記事もありますので合わせてご参照ください。

https://shinagawa-web.com/blogs/react-testing-library-ui-testing

Storybook との統合

Storybook は UI コンポーネントをカタログ化し、開発中に単体で動作を確認できるツールです。特にデザインや実装の確認、コンポーネントの振る舞いのテストに有用で、ビジュアルリグレッションテストや UI インタラクションテストにも活用できます。

Storybook の導入

Storybook をプロジェクトに追加するには、以下のコマンドを実行します。

npx storybook init

このコマンドを実行すると、プロジェクトに Storybook の設定ファイルやサンプルストーリーが自動生成されます。
また、必要な依存関係もインストールされます。

  • stories/ ディレクトリが作成され、サンプルコンポーネントの Story が用意される
  • storybook/main.jsstorybook/preview.js が設定ファイルとして生成される

Storybook の起動

npm run storybook

または

yarn storybook

実行すると、localhost:6006 で Storybook が開きます。

基本的な Story の作成

Storybook では、コンポーネントごとに「Story」を作成して、その状態やバリエーションを管理します。

import { Button } from "../components/Button";

export default {
  title: "Button",
  component: Button,
};

export const Default = () => <Button>Click me</Button>;

Story の構造

  • title: Story のカテゴリを指定("Button" の場合、Button カテゴリの中に Story が表示される)
  • component: Story で使用するコンポーネント
  • export const Default: デフォルトの Story(ここでは Button コンポーネントのデフォルトの状態)

この Story を作成すると、Storybook 上で ButtonDefault バージョンが表示され、クリックなどの動作を手動で確認できるようになります。

Storybook を活用したビジュアルテスト

Storybook では、コンポーネントの UI に変更が加わった際に、意図しない変更がないかを確認するための ビジュアルテスト が@storybook/addon-interactions を利用すると可能となります。

インストール

npm install @storybook/addon-storyshots @storybook/react

設定

src/storyshots.tet.ts
import initStoryshots from "@storybook/addon-storyshots";

initStoryshots();

Storyshots の動作

  • Jest が storyshots.test.ts を実行すると、Storybook のすべての Story のスナップショットが作成される
  • __snapshots__ フォルダ内に .snap ファイルが保存され、次回のテスト実行時に変更がないかを比較する
  • UI に変更があると Jest が差分を検出し、テストが失敗する(意図的な変更なら jest --updateSnapshot でスナップショットを更新)

インタラクションテスト

Storybook は @storybook/addon-interactions を利用すると、簡単な UI インタラクションテストも可能です。

import { within, userEvent, screen } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { Button } from "../components/Button";

export default {
  title: "Button",
  component: Button,
};

export const Clickable = () => <Button onClick={() => alert("Clicked!")}>Click me</Button>;

Clickable.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);

  // クリック前に "Click me" が存在することを明示的にテスト
  expect(await screen.findByText("Click me")).toBeInTheDocument();

  // クリック動作をシミュレート
  await userEvent.click(canvas.getByText("Click me"));
};

play 関数を使ったインタラクションテスト

  • play 関数を定義することで、ストーリー上でインタラクション(クリックや入力)をシミュレートできる
  • within(canvasElement) を使って Story の DOM を取得し、そこに対して userEvent を適用
  • クリックイベントを発火させて、意図通りの動作をするか確認可能

これにより、Storybook 上で UI の操作を試すだけでなく、UI の動作を自動テストとして組み込むことが可能 になります。

Storybook のアドオン活用

Storybook にはさまざまな アドオン があります。代表的なものを紹介します。

  1. @storybook/addon-essentials
    • controls: Storybook 上で Props を操作できる UI を提供
    • actions: コンポーネントのイベント(onClick など)が発火されたことをログ表示
    • docs: Story のドキュメント自動生成

インストール

npm install @storybook/addon-essentials

設定 storybook/main.js に追加

module.exports = {
  addons: ["@storybook/addon-essentials"],
};
  1. @storybook/addon-a11y
    • コンポーネントの アクセシビリティチェック を行うアドオン
npm install @storybook/addon-a11y

設定 storybook/main.js に追加

module.exports = {
  addons: ["@storybook/addon-a11y"],
};

Story に適用

import { Button } from "../components/Button";
import { withA11y } from "@storybook/addon-a11y";

export default {
  title: "Button",
  component: Button,
  decorators: [withA11y],
};

これにより、Storybook 上でアクセシビリティの問題点(コントラスト不足やラベル不足など)を検出できます。

E2E テストの拡充(Playwright / Cypress)

E2E(End-to-End)テストは、アプリケーション全体の動作を検証するためのテスト手法です。ブラウザを使ったユーザーの操作を自動化し、UI が期待通りに動作するかを確認するために利用されます。

Playwright を活用したブラウザテスト

Playwright の特徴

  • 複数ブラウザ対応:Chromium、Firefox、WebKit など主要なブラウザでのテストが可能
  • ヘッドレスモード対応:GUI を表示せずに高速なテスト実行が可能
  • モバイルエミュレーション:特定のデバイス環境をエミュレート可能
  • 並列実行:複数のテストを並列で実行し、効率的にテストを回せる

Playwright の導入

npx playwright install

これにより、必要なブラウザや Playwright の依存関係がインストールされます。

サンプルテスト

以下のサンプルは、http://localhost:3000 にアクセスし、ページのタイトルを検証するテストです。

import { test, expect } from "@playwright/test";

test("Home page has correct title", async ({ page }) => {
  await page.goto("http://localhost:3000");
  await expect(page).toHaveTitle(/My App/);
});

テストを実行するには、以下のコマンドを使用します。

npx playwright test

Cypress を活用した E2E テスト

Cypress の特徴

  • 直感的な API:シンプルで分かりやすい API を提供
  • リアルタイムデバッグ:テスト実行時に UI を確認しながらデバッグ可能
  • シングルブラウザ対応:主に Chromium 系ブラウザ(Chrome、Edge)でのテストに強い
  • スナップショット機能:テストの途中状態をキャプチャしてデバッグを容易にする

Cypress の導入

Cypress をプロジェクトに追加するには、以下のコマンドを実行します。

npm install --save-dev cypress

インストール後、Cypress を起動するには以下のコマンドを実行します。

npx cypress open

サンプルテスト

以下のサンプルは、トップページ (/) から about ページへ遷移するナビゲーションをテストします。

describe("Navigation", () => {
  it("should navigate to the about page", () => {
    cy.visit("/");
    cy.get("a[href='/about']").click();
    cy.url().should("include", "/about");
  });
});

テストをヘッドレスモードで実行するには、以下のコマンドを使用します。

npx cypress run

Playwright と Cypress の比較

項目 Playwright Cypress
ブラウザ対応 Chromium, Firefox, WebKit Chromium 系のみ
並列実行 可能 制限あり(商用版でサポート)
デバイスエミュレーション 可能(モバイル環境エミュレーション可) 限定的
API のシンプルさ やや複雑 シンプルで直感的
デバッグ機能 コードベースのデバッグが強力 UI ベースのデバッグが強力
ヘッドレスモード サポート サポート
セットアップの手軽さ 依存関係が多い インストール後すぐに実行可能

どちらを選ぶべきか?

多様なブラウザでのテストが必要 → Playwright
シンプルな UI テストをすぐに書きたい → Cypress
並列実行やモバイルテストを活用したい → Playwright
視覚的なデバッグを行いたい → Cypress

Playwright は 汎用性が高く多機能なツール であり、Cypress は 初心者でも扱いやすくデバッグしやすい のが特徴です。プロジェクトの要件に応じて選択するとよいでしょう。

Playwrightの導入手順についてのブログ記事もありますので合わせてご参照ください。

https://shinagawa-web.com/blogs/playwright-e2e-testing-introduction

API テストの自動化(Supertest / MSW)

Supertest を活用した API テスト

Supertest は、Node.js ベースのアプリケーションの API テストに最適なライブラリです。特に、ExpressGraphQL API のエンドポイントの動作確認に適しています。

Supertest の導入

Supertest は superagent に基づいており、HTTP リクエストを簡単にテストできます。devDependencies に追加するには以下を実行します。

npm install --save-dev supertest

また、jest などのテストフレームワークと組み合わせて使用することが一般的です。

Supertest の基本的な使い方

以下は、Express サーバーの GET /api エンドポイントをテストするシンプルな例です。

import request from "supertest";
import app from "../server";

test("GET /api returns 200", async () => {
  const response = await request(app).get("/api");
  expect(response.status).toBe(200);
  expect(response.body).toEqual({ message: "Hello, world!" });
});

ポイント

  • request(app).get("/api")
    • Express アプリ (app) に対して GET /api リクエストを送信。
  • expect(response.status).toBe(200);
    • HTTP ステータスコードが 200 OK であることを検証。
  • expect(response.body).toEqual({ message: "Hello, world!" });
    • レスポンスの JSON 内容を検証。

POST リクエストの場合、send() メソッドでリクエストボディを送信できます。

test("POST /api/data returns 201", async () => {
  const response = await request(app)
    .post("/api/data")
    .send({ name: "Test Data" })
    .set("Content-Type", "application/json");

  expect(response.status).toBe(201);
  expect(response.body).toEqual({ id: 1, name: "Test Data" });
});

ポイント

  • .send({ name: "Test Data" }) でリクエストボディを送信
  • .set("Content-Type", "application/json") で適切なヘッダーを指定
  • レスポンスの statusbody の内容を検証

GraphQL API のテストも簡単にできます。

test("GraphQL query returns expected response", async () => {
  const response = await request(app)
    .post("/graphql")
    .send({
      query: `{ user(id: 1) { name email } }`
    })
    .set("Content-Type", "application/json");

  expect(response.status).toBe(200);
  expect(response.body).toEqual({
    data: {
      user: { name: "John Doe", email: "john@example.com" }
    }
  });
});

ポイント

  • GraphQL のリクエストでは query を JSON 形式で送信
  • レスポンスの data フィールドを検証

Supertestの導入手順についてのブログ記事もありますので合わせてご参照ください。

https://shinagawa-web.com/blogs/supertest-jest-express-mongodb-end-to-end-testing

4.2 MSW を活用したモック API テスト

Mock Service Worker(MSW)は、ブラウザや Node.js 環境で API のモックを行うためのライブラリです。特に フロントエンドのテスト で、バックエンドが未実装の状態でも API 呼び出しをエミュレートできます。

MSW の導入

MSW をインストールします。

npm install --save-dev msw

テスト環境で msw/node を使用して API をモックできます。

import { setupServer } from "msw/node";
import { rest } from "msw";

// モック API の定義
const server = setupServer(
  rest.get("/api", (req, res, ctx) => {
    return res(ctx.json({ message: "Hello, world!" }));
  })
);

// テスト実行前にサーバーを起動
beforeAll(() => server.listen());

// テスト実行後にサーバーを閉じる
afterAll(() => server.close());

// 各テストの後にリクエストハンドラをリセット
afterEach(() => server.resetHandlers());

test("GET /api returns mock response", async () => {
  const response = await fetch("/api");
  const data = await response.json();

  expect(response.status).toBe(200);
  expect(data).toEqual({ message: "Hello, world!" });
});

ポイント

  • setupServer() を使って Node.js のモックサーバーを作成
  • server.listen() でテスト開始時にモックサーバーを起動
  • server.close() でテスト終了時にサーバーを停止
  • server.resetHandlers() でモックの設定をクリア

ブラウザ環境では、setupWorker() を使用して API をモックできます。

import { setupWorker } from "msw";
import { rest } from "msw";

export const worker = setupWorker(
  rest.get("/api", (req, res, ctx) => {
    return res(ctx.json({ message: "Hello, world!" }));
  })
);

// 開発環境でモックを有効化
worker.start();

フロントエンドでの利用方法

  • worker.start();index.tsxApp.tsx のエントリーポイントで実行
  • 開発時にバックエンドなしで API 呼び出しが動作
  • Network タブでモック API のリクエスト・レスポンスを確認可能

Supertest と MSW の使い分け

Supertest MSW
用途 バックエンド API のテスト(Express / GraphQL) フロントエンドの開発・テスト
環境 Node.js ブラウザ & Node.js
リクエスト送信 request(app).get("/api") fetch("/api")
レスポンスの検証 expect(response.status).toBe(200) expect(data).toEqual({...})
バックエンドの実装が必要か 必要(実際の API が動作する) 不要(API をエミュレートできる)

ポイント

  • Supertest はバックエンドの API エンドポイントのテスト に最適
  • MSW は フロントエンドの開発・テスト で API をモックするのに便利
  • どちらも Jest や Playwright などのテスト環境と組み合わせ可能

MSWの導入手順についてのブログ記事もありますので合わせてご参照ください。

https://shinagawa-web.com/blogs/msw-api-mock-and-test-automation

モックデータの整理と最適化

開発中のフロントエンドとバックエンドが揃わない場合や、安定したテスト環境を構築するために、モックデータの整理と最適化は重要な課題です。特に以下のポイントに着目すると効果的です。

Faker を活用したダミーデータの生成

Faker を使うことで、リアルなテストデータを手軽に生成できます。ただし、毎回異なる値が生成されるとテストが不安定になるため、faker.seed() を活用し、一貫性のあるデータを出力できるようにします。

@faker-js/faker をインストールします。

npm install @faker-js/faker

基本的なダミーデータの生成

import { faker } from '@faker-js/faker';

// 予測可能なデータを生成するためのシード値
faker.seed(123);

const user = {
  id: faker.string.uuid(),
  name: faker.person.fullName(),
  email: faker.internet.email(),
  phone: faker.phone.number(),
};

console.log(user);

このコードを実行すると、毎回同じデータが生成されるため、テストの再現性が保たれるようになります。

逆に重複のないデータを作りたい場合、faker.helpers.unique() を使うのが有効です。

const uniqueEmails = new Set();

for (let i = 0; i < 5; i++) {
  uniqueEmails.add(faker.helpers.unique(faker.internet.email));
}

console.log([...uniqueEmails]); // 一意なメールアドレスの配列

GraphQL Mock の適用

GraphQL を活用する場合、バックエンドの API が未完成の段階でも、フロントエンド開発を進めるために @graphql-tools/mock を活用してモックサーバーを構築します。

必要なライブラリをインストールします。

npm install @graphql-tools/mock @graphql-tools/schema graphql

GraphQL のスキーマを作成し、モックデータを提供します。

import { makeExecutableSchema } from '@graphql-tools/schema';
import { addMocksToSchema } from '@graphql-tools/mock';
import { graphql } from 'graphql';
import { faker } from '@faker-js/faker';

// GraphQL スキーマの定義
const typeDefs = `
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    users: [User!]!
  }
`;

// モックレスポンスの定義
const mocks = {
  User: () => ({
    id: faker.string.uuid(),
    name: faker.person.fullName(),
    email: faker.internet.email(),
  }),
};

// モック付きのスキーマを作成
const schema = makeExecutableSchema({ typeDefs });
const schemaWithMocks = addMocksToSchema({ schema, mocks });

// クエリを実行
const query = `
  query {
    users {
      id
      name
      email
    }
  }
`;

graphql({ schema: schemaWithMocks, source: query }).then((result) =>
  console.log(JSON.stringify(result, null, 2))
);

ポイント

  • GraphQL のスキーマに基づいたモックを提供できるため、API 設計に即した開発ができる。
  • @graphql-tools/mock を利用することで、レスポンスを 動的に変更できる。

カスタムリゾルバを適用し、動的なデータを提供

デフォルトの @graphql-tools/mock は、同じデータを返し続けることがあります。そのため、カスタムリゾルバを適用してよりリアルな挙動を作ることができます。

import { makeExecutableSchema } from '@graphql-tools/schema';
import { addMocksToSchema } from '@graphql-tools/mock';
import { graphql } from 'graphql';
import { faker } from '@faker-js/faker';

// スキーマ定義
const typeDefs = `
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    users: [User!]!
  }
`;

// カスタムリゾルバの定義
const mocks = {
  Query: {
    users: () => {
      return Array.from({ length: 5 }).map(() => ({
        id: faker.string.uuid(),
        name: faker.person.fullName(),
        email: faker.internet.email(),
      }));
    },
  },
};

// モック付きのスキーマを作成
const schema = makeExecutableSchema({ typeDefs });
const schemaWithMocks = addMocksToSchema({ schema, mocks });

// クエリの実行
const query = `
  query {
    users {
      id
      name
      email
    }
  }
`;

graphql({ schema: schemaWithMocks, source: query }).then((result) =>
  console.log(JSON.stringify(result, null, 2))
);

メリット

  • クエリごとに異なるデータを生成できます。
  • データを動的にカスタマイズできるため、テストシナリオを増やせます。

@graphql-tools/mockFaker の利用ガイドについてのブログ記事もありますので合わせてご参照ください。

https://shinagawa-web.com/blogs/mock-server-graphql-tools-faker

CI/CD でのテスト実行フローの最適化

CI/CD でのテスト実行時間を短縮するために、並列実行とキャッシュの活用が欠かせません。

テストの並列実行

テストの並列実行により、CI/CD の実行時間を大幅に短縮できます。

Jest の並列実行
Jest はデフォルトで並列実行を行いますが、--max-workers を調整することで、並列数を制御できます。

jest --max-workers=50%
  • --max-workers=50%:CPU の 50% を使う(負荷を軽減しつつ並列実行)
  • --max-workers=4:最大 4 並列で実行

CI の環境変数を考慮して動的に設定することも可能です。

jest --max-workers=$(nproc)

nproc は利用可能な CPU コア数を取得する Linux コマンド)

GitHub Actions の matrix 機能を使った並列実行

GitHub Actions では matrix を活用して、異なるテストケースを並列に実行できます。

例えば、以下のようにテストファイルをグループ分けし、それぞれを並列実行できます。

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4] # 4 つに分割
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests (sharded)
        run: |
          TEST_FILES=$(jest --listTests | sort | awk "NR % 4 == $(( ${{ matrix.shard }} - 1 ))")
          jest $TEST_FILES --max-workers=2
  • jest --listTests で全テストファイルを取得し、4 分割して並列実行。
  • --max-workers=2 で並列実行の上限を 2 に設定。

キャッシュの活用

テスト実行の高速化には、キャッシュを適切に活用することが重要です。

node_modules のキャッシュ

依存関係のインストールを高速化するために、node_modules をキャッシュできます。

GitHub Actions の場合

- name: Cache node_modules
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: npm-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      npm-
  • package-lock.json が変更されない限りキャッシュを利用します。

CircleCI の場合

- restore_cache:
    keys:
      - npm-deps-{{ checksum "package-lock.json" }}
- run: npm ci
- save_cache:
    key: npm-deps-{{ checksum "package-lock.json" }}
    paths:
      - ~/.npm

Jest のキャッシュ

Jest はテスト結果をキャッシュできるため、--cache を有効化すると、再実行時に不要なテストをスキップできます。

jest --cache

また、キャッシュディレクトリを GitHub Actions で保存することで、次回の実行時に活用できます。

- name: Cache Jest cache
  uses: actions/cache@v3
  with:
    path: .jest/cache
    key: jest-${{ github.run_id }}
    restore-keys: |
      jest-

Prisma のキャッシュ

prisma generate は DB スキーマから TypeScript 型を生成するため、実行コストが高いですが、キャッシュを活用できます。

GitHub Actions の場合

- name: Cache Prisma
  uses: actions/cache@v3
  with:
    path: node_modules/.prisma
    key: prisma-${{ hashFiles('prisma/schema.prisma') }}
    restore-keys: |
      prisma-

CircleCI の場合

- restore_cache:
    keys:
      - prisma-cache-{{ checksum "prisma/schema.prisma" }}
- run: npx prisma generate
- save_cache:
    key: prisma-cache-{{ checksum "prisma/schema.prisma" }}
    paths:
      - node_modules/.prisma

依存関係を管理したテストの設計(環境ごとにテストを分離)

テストの信頼性を向上させるために、環境ごとにテストを分離し、適切に管理することが重要です。
以下のように、テストの種類ごとに環境を分け、依存関係を明確に制御するのが理想的です。

ユニットテスト(Unit Test)

  • 目的
    • 個々の関数やクラスが期待通り動作するかを検証する。
  • 依存関係
    • 外部リソース(DB・API・ファイルシステムなど)に依存せず、すべてモック化する。
  • 実行環境
    • ローカルで高速に実行(CI でも並列実行可能)

設計のポイント

  • 外部依存(DB・API・ストレージ)をモック化
  • 状態を保持せず、独立したテストケース
  • テストデータは最小限
  • 高速実行(数ms 〜 数百ms)

例: API レスポンスをモック化

import { fetchUser } from '../src/userService';
import axios from 'axios';

jest.mock('axios');

describe('fetchUser', () => {
  it('should return user data', async () => {
    (axios.get as jest.Mock).mockResolvedValue({ data: { id: 1, name: 'Alice' } });

    const user = await fetchUser(1);
    expect(user).toEqual({ id: 1, name: 'Alice' });
  });
});

ポイント

  • axios をモック化し、外部 API への依存を排除
  • データベースに接続せず、関数単体の動作を保証

統合テスト(Integration Test)

  • 目的
    • 複数のコンポーネントやサービスが連携して正しく動作するかを検証する。
  • 依存関係
    • 実際のデータベース・API などを使用するが、環境ごとに切り替え可能にする。
  • 実行環境
    • CI/CD 上で docker-compose を活用し、依存関係を統一する。

設計のポイント

  • 実際の DB・API との接続テスト
  • データのセットアップとクリーンアップ
  • ローカルと CI で同じ環境を構築(Docker で統一)

例)PostgreSQL を用いた統合テスト

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

describe('User Repository Integration Test', () => {
  beforeAll(async () => {
    await prisma.$connect();
  });

  afterAll(async () => {
    await prisma.$disconnect();
  });

  beforeEach(async () => {
    await prisma.user.deleteMany(); // データリセット
  });

  it('should create and retrieve a user', async () => {
    await prisma.user.create({ data: { name: 'Alice' } });
    const users = await prisma.user.findMany();
    expect(users).toHaveLength(1);
    expect(users[0].name).toBe('Alice');
  });
});

ポイント

  • データベースを実際に操作し、エンドツーエンドでの動作を確認
  • beforeEach でデータをリセットし、テスト間の影響を防ぐ

Docker で DB を統一します。

version: '3.8'
services:
  postgres:
    image: postgres:14
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: test_db
    ports:
      - "5432:5432"

ポイント

  • ローカルでも CI 環境でも docker-compose up で統一した環境を構築

PostgreSQL の接続先の定義方法

  1. .env.test でテスト用のデータベースを定義
    統合テストでは、本番環境のデータとは別にテスト専用のデータベースを使用するのが推奨されます。そのため、テスト用の .env.test を作成し、接続先を分離します。

.env.test の例

DATABASE_URL=postgresql://test_user:test_password@localhost:5432/test_db?schema=public
  1. Jest 実行時に .env.test を適用
    Prisma はデフォルトで .env を読み込みますが、テスト環境用の .env.test を適用するには、Jest の setupFiles で dotenv を読み込むようにします。

jest.setup.ts を作成

jest.setup.ts
import dotenv from 'dotenv';

// `.env.test` を優先して読み込む
dotenv.config({ path: '.env.test' });

次に、Jest の setupFiles にこのファイルを指定します。

jest.config.js

jest.config.js
module.exports = {
  setupFiles: ['<rootDir>/jest.setup.ts'],
};

E2E テスト(End-to-End Test)

  • 目的
    • ユーザーの操作をシミュレートし、システム全体が期待通り動作するかを検証する。
  • 依存関係
    • 本番に近い環境(staging)を用意し、API・DB すべて実際のものを利用。
  • 実行環境
    • stagingproduction を分離し、環境変数で切り替え。
    • PlaywrightCypress を利用。

設計のポイント

  • ブラウザ操作を自動化し、実際のユーザー操作をテスト
  • staging 環境で実行し、production とは分離
  • ログ・スクリーンショットを保存してデバッグしやすく

例) Playwright を用いたログインテスト

import { test, expect } from '@playwright/test';

test('ログイン成功時の挙動を確認', async ({ page }) => {
  await page.goto('https://staging.example.com/login');

  await page.fill('input[name="email"]', 'test@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('https://staging.example.com/dashboard');
});

ポイント

  • ブラウザ操作を自動化し、実際の動作をテスト
  • 環境変数で staging と production を切り替え

環境変数で接続先を切り替え

.env.staging
BASE_URL=https://staging.example.com
.env.production
BASE_URL=https://example.com

テスト種類まとめ

テスト種類 目的 依存関係 実行環境
ユニットテスト 関数・クラス単体の動作検証 すべてモック化 ローカル・CI
統合テスト サービス間の連携を検証 実際の DB・API 使用 CI (docker-compose)
E2E テスト ユーザー操作をシミュレート staging 環境利用 staging or production

テストカバレッジの可視化(Codecov / SonarQube の適用)

テストカバレッジの可視化は、コードの品質維持・向上に不可欠です。特に、以下の2つのツール Codecov と SonarQube を活用することで、コードの状態をより詳細に把握できます。

Codecov の活用

Codecov は、テストカバレッジ(どのコードがテストされているか)を可視化し、GitHub などのリポジトリと連携してカバレッジの変化をトラッキングできるサービスです。

  • PR ごとのカバレッジ変化を可視化
  • 複数の CI/CD と連携可能
  • GitHub のコメントでカバレッジレポートを自動投稿
  • Web ダッシュボードでカバレッジの推移を分析

また、CI/CD でカバレッジを管理する Codecov のメリットとしては下記が挙げられます。

  • GitHub の PR にカバレッジ変化を表示
  • Web UI で過去のカバレッジ履歴を確認
  • プロジェクト全体のカバレッジ低下を防げる
  • 複数のテストフレームワーク(Jest, Mocha, Pytest など)と統合できる
  1. Codecov のセットアップ
    Codecov をプロジェクトで使用するためには、以下の手順を実施します。

Codecov のアカウント作成
Codecov の公式サイトで GitHub 連携を行い、リポジトリを登録します。

https://about.codecov.io

  1. codecov-action を GitHub Actions に追加
    GitHub Actions のワークフローに codecov-action を追加し、テスト実行後にカバレッジレポートをアップロードします。

例:GitHub Actions (.github/workflows/test.yml)

name: Run Tests and Upload Coverage

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: Install dependencies
        run: npm install

      - name: Run tests with coverage
        run: npm test -- --coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }} # Codecov のトークン(リポジトリの Secrets に登録)
  1. jest.config.js のカバレッジ設定
    Jest を使用している場合、カバレッジを出力するように jest.config.js に設定を追加します。
jest.config.js
module.exports = {
  collectCoverage: true,
  collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"],
  coverageDirectory: "coverage",
  coverageReporters: ["json", "lcov", "text", "clover"],
};
  1. Codecov レポートの確認
    PR を作成すると、Codecov が自動的にカバレッジレポートを生成し、GitHub 上で確認できます。
  • ファイルごとのカバレッジ率
  • 行単位のカバレッジ
  • 変更によるカバレッジの増減

PRに分析結果を表示することも可能

Image from Gyazo

参考:

https://about.codecov.io/blog/find-failing-and-flaky-tests-with-codecov-test-analytics/

どんなときに Codecov を使うべきか?

状況 Jest だけで OK Codecov が便利
ローカルでカバレッジ確認
小規模プロジェクト(個人開発)
CI/CD にカバレッジを組み込みたい △(手間がかかる)
PR ごとにカバレッジの変化を確認したい
しきい値(80% 以下で CI 失敗)を設定したい △(可能だが面倒)
カバレッジの履歴を見たい

SonarQube の導入

SonarQube は、テストカバレッジだけでなく、コード品質やセキュリティホールの検出、静的解析を行うツールです。

  1. SonarCloud または SonarQube の選択
    SonarCloud(クラウド版)

https://sonarcloud.io/

SonarQube(オンプレミス版)

https://www.sonarqube.org/

  1. sonar-scanner のインストール
npm install sonar-scanner
  1. SonarQube 設定ファイルの作成 (sonar-project.properties)
sonar.projectKey=your_project_key
sonar.organization=your_organization
sonar.host.url=https://sonarcloud.io
sonar.token=your_sonar_token

sonar.sources=src
sonar.exclusions=**/*.spec.js, **/*.test.js
sonar.tests=tests
sonar.test.inclusions=**/*.spec.js, **/*.test.js
sonar.javascript.lcov.reportPaths=coverage/lcov.info
  1. GitHub Actions で sonar-scanner を実行
- name: Run SonarQube Scan
  run: sonar-scanner
  1. SonarQube レポートの確認
  • コードの品質スコア
  • バグやセキュリティホール
  • コードの重複率

フィーチャーフラグを考慮したテスト

開発中の機能と既存機能を共存させながらテストするために、フィーチャーフラグを考慮した戦略が必要です。

小規模プロジェクトでは、環境変数を利用してフィーチャーフラグを管理できます。

例:.env

FEATURE_NEW_DASHBOARD=true

例:config.ts

export const featureFlags = {
  newDashboard: process.env.FEATURE_NEW_DASHBOARD === "true",
};

フィーチャーフラグを考慮したテスト

フィーチャーフラグが ON/OFF の場合に応じたテストケースを用意することが重要です。

  1. フィーチャーフラグの管理
    まず、フィーチャーフラグの状態を管理する関数を実装します。

例:featureFlags.ts

export const featureFlags = {
  newAlgorithm: process.env.FEATURE_NEW_ALGORITHM === "true",
};

export function isFeatureEnabled(flag: keyof typeof featureFlags): boolean {
  return featureFlags[flag];
}
  • featureFlags にフィーチャーフラグを定義(環境変数で管理)
  • isFeatureEnabled(flag: string) で、フラグの ON/OFF を判定する
  1. フィーチャーフラグを利用する関数
    次に、このフィーチャーフラグを使って動作を変える関数を実装します。

例:calculateDiscount.ts

calculateDiscount.ts
import { isFeatureEnabled } from "./featureFlags";

export function calculateDiscount(price: number): number {
  if (isFeatureEnabled("newAlgorithm")) {
    // 新アルゴリズムの適用
    return price * 0.9; // 10% 割引
  } else {
    // 旧アルゴリズムの適用
    return price * 0.95; // 5% 割引
  }
}
  • isFeatureEnabled("newAlgorithm") でフラグを判定
  • フラグが ON なら 10% 割引
  • フラグが OFF なら 5% 割引
  1. 実装した calculateDiscount() の関数を、フィーチャーフラグの ON/OFF を切り替えてテストします。
import { calculateDiscount } from "../calculateDiscount";
import * as featureFlags from "../featureFlags"; // モジュールを import する

describe("calculateDiscount", () => {
  afterEach(() => {
    jest.restoreAllMocks(); // モックのリセット
  });

  test("フィーチャーフラグが ON の場合、新アルゴリズム (10% 割引) が適用される", () => {
    jest.spyOn(featureFlags, "isFeatureEnabled").mockReturnValue(true);

    expect(calculateDiscount(1000)).toBe(900); // 1000円の10%引き → 900円
  });

  test("フィーチャーフラグが OFF の場合、旧アルゴリズム (5% 割引) が適用される", () => {
    jest.spyOn(featureFlags, "isFeatureEnabled").mockReturnValue(false);

    expect(calculateDiscount(1000)).toBe(950); // 1000円の5%引き → 950円
  });
});

ポイント

  • jest.spyOn(featureFlags, "isFeatureEnabled") で フラグの ON/OFF をモック
  • フラグ ON:10% 割引
  • フラグ OFF:5% 割引
  • afterEach でモックをリセットし、他のテストに影響を与えないようにする

さいごに

テストの自動化は、プロジェクトの規模が大きくなるにつれて不可欠な要素となります。しかし、テストをただ増やせばよいわけではなく、適切な戦略をもって導入し、運用・最適化していくことが重要です。

本記事では、ユニットテストから E2E テスト、API テスト、モックデータ管理、CI/CD におけるテストの最適化まで、実践的なテスト戦略を紹介しました。これらの手法を適用することで、開発スピードを落とさずに品質を向上させることが可能 です。

テストは「書いて終わり」ではなく、「継続的に改善しながら運用するもの」です。適切なツールと手法を活用し、プロジェクトに適したテスト戦略を構築していきましょう。

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ