はじめに
ソフトウェア開発において、テストの自動化は品質を維持しながら開発スピードを向上させる重要な要素です。しかし、テストの導入や運用には「どこまでテストすべきか」「適切なツールは何か」「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
を作成します。
import { Config } from "jest";
const config: Config = {
preset: "ts-jest",
testEnvironment: "node",
};
export default config;
Jest によるユニットテスト
- 関数のユニットテスト
例えば、math.ts に sum 関数を実装するとします。
export function sum(a: number, b: number): number {
return a + b;
}
この sum
関数をテストするには、math.test.ts
を作成します。
import { sum } from "./math";
test("sum correctly adds two numbers", () => {
expect(sum(1, 2)).toBe(3);
});
Jest を実行するには、以下のコマンドを使用します。
npx jest
- クラスのユニットテスト
クラスのテストも容易に行えます。例えば、Counter クラスを作成します。
export class Counter {
private count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
getCount(): number {
return this.count;
}
}
このクラスのテストを作成します。
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);
});
- 非同期処理のテスト
非同期関数の動作を確認するために、async/await を使ったテストも可能です。
export async function fetchData(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve("Hello World"), 1000);
});
}
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
を作成します。
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
},
});
Vitest によるユニットテスト
Vitest では、Jest とほぼ同じ書き方でテストが書けます。
- 関数のテスト
import { describe, it, expect } from "vitest";
import { sum } from "./math";
describe("sum function", () => {
it("correctly adds two numbers", () => {
expect(sum(1, 2)).toBe(3);
});
});
- クラスのテスト
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);
});
});
- 非同期処理のテスト
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の導入手順についてのブログ記事もありますので合わせてご参照ください。
コンポーネントテストの最適化(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の導入手順についてのブログ記事もありますので合わせてご参照ください。
Storybook との統合
Storybook は UI コンポーネントをカタログ化し、開発中に単体で動作を確認できるツールです。特にデザインや実装の確認、コンポーネントの振る舞いのテストに有用で、ビジュアルリグレッションテストや UI インタラクションテストにも活用できます。
Storybook の導入
Storybook をプロジェクトに追加するには、以下のコマンドを実行します。
npx storybook init
このコマンドを実行すると、プロジェクトに Storybook の設定ファイルやサンプルストーリーが自動生成されます。
また、必要な依存関係もインストールされます。
stories/
ディレクトリが作成され、サンプルコンポーネントのStory
が用意されるstorybook/main.js
やstorybook/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 上で Button
の Default
バージョンが表示され、クリックなどの動作を手動で確認できるようになります。
Storybook を活用したビジュアルテスト
Storybook では、コンポーネントの UI に変更が加わった際に、意図しない変更がないかを確認するための ビジュアルテスト が@storybook/addon-interactions
を利用すると可能となります。
インストール
npm install @storybook/addon-storyshots @storybook/react
設定
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 にはさまざまな アドオン があります。代表的なものを紹介します。
@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"],
};
- @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の導入手順についてのブログ記事もありますので合わせてご参照ください。
API テストの自動化(Supertest / MSW)
Supertest を活用した API テスト
Supertest は、Node.js
ベースのアプリケーションの API テストに最適なライブラリです。特に、Express
や GraphQL 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")
で適切なヘッダーを指定- レスポンスの
status
やbody
の内容を検証
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の導入手順についてのブログ記事もありますので合わせてご参照ください。
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.tsx
やApp.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の導入手順についてのブログ記事もありますので合わせてご参照ください。
モックデータの整理と最適化
開発中のフロントエンドとバックエンドが揃わない場合や、安定したテスト環境を構築するために、モックデータの整理と最適化は重要な課題です。特に以下のポイントに着目すると効果的です。
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/mock
と 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 の接続先の定義方法
- .env.test でテスト用のデータベースを定義
統合テストでは、本番環境のデータとは別にテスト専用のデータベースを使用するのが推奨されます。そのため、テスト用の .env.test を作成し、接続先を分離します。
.env.test
の例
DATABASE_URL=postgresql://test_user:test_password@localhost:5432/test_db?schema=public
- Jest 実行時に .env.test を適用
Prisma はデフォルトで .env を読み込みますが、テスト環境用の .env.test を適用するには、Jest の setupFiles で dotenv を読み込むようにします。
jest.setup.ts を作成
import dotenv from 'dotenv';
// `.env.test` を優先して読み込む
dotenv.config({ path: '.env.test' });
次に、Jest の setupFiles にこのファイルを指定します。
jest.config.js
module.exports = {
setupFiles: ['<rootDir>/jest.setup.ts'],
};
E2E テスト(End-to-End Test)
- 目的
- ユーザーの操作をシミュレートし、システム全体が期待通り動作するかを検証する。
- 依存関係
- 本番に近い環境(staging)を用意し、API・DB すべて実際のものを利用。
- 実行環境
staging
とproduction
を分離し、環境変数で切り替え。Playwright
やCypress
を利用。
設計のポイント
- ブラウザ操作を自動化し、実際のユーザー操作をテスト
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 を切り替え
環境変数で接続先を切り替え
BASE_URL=https://staging.example.com
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 など)と統合できる
- Codecov のセットアップ
Codecov をプロジェクトで使用するためには、以下の手順を実施します。
Codecov のアカウント作成
Codecov の公式サイトで GitHub 連携を行い、リポジトリを登録します。
- 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 に登録)
- jest.config.js のカバレッジ設定
Jest を使用している場合、カバレッジを出力するように jest.config.js に設定を追加します。
module.exports = {
collectCoverage: true,
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"],
coverageDirectory: "coverage",
coverageReporters: ["json", "lcov", "text", "clover"],
};
- Codecov レポートの確認
PR を作成すると、Codecov が自動的にカバレッジレポートを生成し、GitHub 上で確認できます。
- ファイルごとのカバレッジ率
- 行単位のカバレッジ
- 変更によるカバレッジの増減
PRに分析結果を表示することも可能
参考:
どんなときに Codecov を使うべきか?
状況 | Jest だけで OK | Codecov が便利 |
---|---|---|
ローカルでカバレッジ確認 | ✅ | ❌ |
小規模プロジェクト(個人開発) | ✅ | ❌ |
CI/CD にカバレッジを組み込みたい | △(手間がかかる) | ✅ |
PR ごとにカバレッジの変化を確認したい | ❌ | ✅ |
しきい値(80% 以下で CI 失敗)を設定したい | △(可能だが面倒) | ✅ |
カバレッジの履歴を見たい | ❌ | ✅ |
SonarQube の導入
SonarQube は、テストカバレッジだけでなく、コード品質やセキュリティホールの検出、静的解析を行うツールです。
- SonarCloud または SonarQube の選択
SonarCloud(クラウド版)
SonarQube(オンプレミス版)
- sonar-scanner のインストール
npm install sonar-scanner
- 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
- GitHub Actions で sonar-scanner を実行
- name: Run SonarQube Scan
run: sonar-scanner
- SonarQube レポートの確認
- コードの品質スコア
- バグやセキュリティホール
- コードの重複率
フィーチャーフラグを考慮したテスト
開発中の機能と既存機能を共存させながらテストするために、フィーチャーフラグを考慮した戦略が必要です。
小規模プロジェクトでは、環境変数を利用してフィーチャーフラグを管理できます。
例:.env
FEATURE_NEW_DASHBOARD=true
例:config.ts
export const featureFlags = {
newDashboard: process.env.FEATURE_NEW_DASHBOARD === "true",
};
フィーチャーフラグを考慮したテスト
フィーチャーフラグが ON/OFF の場合に応じたテストケースを用意することが重要です。
- フィーチャーフラグの管理
まず、フィーチャーフラグの状態を管理する関数を実装します。
例: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 を判定する
- フィーチャーフラグを利用する関数
次に、このフィーチャーフラグを使って動作を変える関数を実装します。
例: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% 割引
- 実装した 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 におけるテストの最適化まで、実践的なテスト戦略を紹介しました。これらの手法を適用することで、開発スピードを落とさずに品質を向上させることが可能 です。
テストは「書いて終わり」ではなく、「継続的に改善しながら運用するもの」です。適切なツールと手法を活用し、プロジェクトに適したテスト戦略を構築していきましょう。
関連する技術ブログ
Webアクセシビリティの完全ガイド:Lighthouse / axe による自動テスト、WCAG基準策定、キーボード操作・スクリーンリーダー対応まで
Webアクセシビリティの課題を解決するための包括的なガイド。Lighthouse / axe を活用した自動テストの設定、WCAGガイドラインに基づく評価基準の整備、キーボード操作やスクリーンリーダー対応の改善、カラーコントラストの最適化、ARIAランドマークの導入、フォームやモーダルの操作性向上まで詳しく解説。定期的なアクセシビリティレポートを活用し、継続的な改善を実現する方法も紹介します。
shinagawa-web.com
JestとTypeScriptで始めるテスト自動化:基本設定から型安全なテストの書き方まで徹底解説
JestとTypeScriptを使ったテスト自動化の基本を学びたい方へ。環境のセットアップ方法、型安全なテストを書くメリット、コードの信頼性を高める実践的なテクニックを初心者向けに丁寧に解説します。テストカバレッジの活用で、品質の高い開発を目指しましょう。
shinagawa-web.com
Mock Service Worker (MSW) を使ったAPIモックとテストの効率化
MSW(Mock Service Worker)を使用して、フロントエンド開発やテスト環境でのAPIモックを効率的に行う方法を解説します。Mock Service Workerの基本的な使い方から、Jestテストでの活用方法、さらにテストを簡単にするための設定手順を紹介します。
shinagawa-web.com
フロントエンド開発で欠かせないReactのUIコンポーネントのテストをReact Testing Libraryで実装
React Testing Libraryを使って、Reactコンポーネントのテストを行う方法を学びます。本記事では、Next.js環境でのセットアップから、ユーザーインタラクションをシミュレーションしたテストコードの作成までを詳しく解説します。コンポーネントが期待通りに動作するかを確認し、実際のアプリケーションに近い形でのテストを実装しましょう。
shinagawa-web.com
Supertest と Jest を活用した Express + MongoDB アプリのエンドツーエンドテスト解説
Supertest と Jest を組み合わせて Express アプリケーションの API テストを効率化する方法を解説します。サービス層の導入やテスト可能なコード設計へのリファクタリング、GET, POST, PATCH, DELETEメソッドのテスト実装まで、具体的なコード例を交えて詳しく紹介します。
shinagawa-web.com
JavaScript から TypeScript への移行ガイド
JavaScript から TypeScript への移行は、コードの型安全性を向上させ、バグの早期発見を可能にする重要な取り組みです。本記事では、10の取り組みについて詳しく解説します。
shinagawa-web.com
可読性・再利用性・パフォーマンスを向上させる!フロントエンドリファクタリングの実践ガイド
フロントエンド開発におけるコードの品質を向上させるためのリファクタリング手法を徹底解説!命名規則や関数分割による可読性向上、共通処理のモジュール化、長大なコンポーネントの分割、設計パターンの適用など、実践的なアプローチを紹介します。さらに、Reactのパフォーマンス改善、非同期処理の最適化、依存関係の整理、TypeScriptの適用範囲拡大まで網羅し、段階的に進められるリファクタリング計画の策定方法も解説。よりメンテナブルで拡張しやすいコードを目指すエンジニア必見のガイドです!
shinagawa-web.com
ExpressとMongoDBで簡単にWeb APIを構築する方法【TypeScript対応】
本記事では、MongoDB Atlasを活用してREST APIを構築する方法を、初心者の方にも分かりやすいステップで解説します。プロジェクトの初期設定からMongoDBの接続設定、Expressを使用したルートの作成、さらにTypeScriptを用いた型安全な実装まで、実践的なサンプルコードを交えて丁寧に説明します。
shinagawa-web.com