【実務・中級編】 単体テストにおけるPropsのモック化 – React実践ガイド

フロントエンドの現場で頭を悩ませる瞬間の一つに、「子コンポーネントがやたらと複雑なPropsを要求してくる問題」がありますよね。特に、APIから生えてきた巨大なドメインモデルをそのまま突っ込んでいるようなコンポーネントのテストを書くとき、私たちは途方に暮れがちです。「たった一つの真偽値の状態変化をテストしたいだけなのに、なぜ30行もあるダミーのユーザーオブジェクトを作らなきゃいけないんだ…!」と。

こんにちは。プロダクトのコードベースが肥大化していく歴史を何度も目撃し、その都度テストの書きやすさとアーキテクチャの美しさを死守してきたシニアエンジニアの私です。

今回は、中級からもう一歩上の「実戦力」を身につけたいあなたに向けて、Propsのモック化と `jest.fn()` を使ったコールバック関数のテストの極意を、ブラウザの裏側の動きや設計思想を交えながら徹底的に解説します。

—

なぜPropsのモック化で躓くのか?

Reactの本質は極めてシンプルです。「UI = f(state, props)」、つまり状態とプロパティという「入力」を、純粋なJavaScriptの関数(コンポーネント)に流し込んでJSXという「仮想DOMツリー」という出力に変換しているだけです。

しかし、実務のコンポーネントは純粋関数だけでは成り立ちません。「ボタンが押されたら親のAPIを叩く」「入力値が変わったら親のバリデーションを走らせる」といった副作用(Side Effect)のトリガーとして、関数型のProps(コールバック)を受け取ります。

単体テスト(Jest + React Testing Libraryなど)において、これら外部依存を完全に遮断し、コンポーネント単体のロジックを担保するために「モック化」が必要になります。ここで雑にコードを書くと、テストが脆くなり(Fragile)、少しの仕様変更でテストがバタバタと倒れる「負の遺産」が生まれます。

—

1. コールバック関数のモック:`jest.fn()` の正しい作法

まずは、子コンポーネントから親へイベントを通知するコールバック関数のテストから始めましょう。

実務でよくある「削除確認モーダル」を例にします。ユーザーが削除ボタンを押したときに、`onConfirm` プロップスが正しく呼ばれるか、そしてその際に予期せぬ引数が渡されていないかを検証します。

実装サンプルコード

// DeleteConfirmDialog.tsx
import React from ‘react’;

type DeleteConfirmDialogProps = {
targetName: string;
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
};

export const DeleteConfirmDialog: React.FC = ({
targetName,
isOpen,
onConfirm,
onCancel,
}) => {
if (!isOpen) return null;

return (

{targetName} を削除してもよろしいですか?

{/ 削除実行ボタン /}

{/ キャンセルボタン /}

);
};

これに対するテストコードを見てみましょう。`jest.fn()` の真価は、単に「呼ばれたかどうか」だけでなく、「何回呼ばれたか」「どんな引数で呼ばれたか」を完全に追跡できる点にあります。

// DeleteConfirmDialog.test.tsx
import React from ‘react’;
import { render, screen } from ‘@testing-library/react’;
import userEvent from ‘@testing-library/user-event’;
import { DeleteConfirmDialog } from ‘./DeleteConfirmDialog’;

describe(‘DeleteConfirmDialog’, () => {
const dummyTargetName = ‘プロジェクトA’;

it(‘「削除する」ボタンをクリックすると、onConfirmコールバックが実行されること’, async () => {
// 1. jest.fn()でモック関数を作成する
const handleConfirm = jest.fn();
const handleCancel = jest.fn();

// 2. コンポーネントにモック関数をPropsとして注入する
render(

);

// 3. ユーザーの実際の操作をシミュレート(userEventを使用)
const confirmButton = screen.getByTestId(‘confirm-button’);
await userEvent.click(confirmButton);

// 4. コールバックが意図通りに呼ばれたか検証する
expect(handleConfirm).toHaveBeenCalledTimes(1);
// キャンセル側が誤って呼ばれていないことも担保する
expect(handleCancel).not.toHaveBeenCalled();
});
});

プロのTips: `fireEvent` ではなく `userEvent` を使え

ブラウザの裏側では、ユーザーがボタンをクリックすると、`pointerover`, `pointerdown`, `mousedown`, `focus`, `mouseup`, `click` といった一連のネイティブイベントが発火します。古くからある `fireEvent` はこれらを単発で雑にシミュレートしますが、`@testing-library/user-event` は実際のブラウザの挙動に近いイベントの連続を再現してくれます。実務では迷わず `userEvent` を選びましょう。

—

2. 複雑なオブジェクトPropsのモック化と「Object Factoryパターン」

次に頭が痛くなるのが、何十個ものプロパティを持つ巨大なオブジェクト型Propsの扱いです。例えば、ユーザー情報、権限、組織ツリー、課金ステータスが入り混じったオブジェクトを考えてみてください。

これをテストごとに毎回手書きで定義していると、次のような地獄が生まれます。

  • TypeScriptの型定義(TypeScriptのアップデートなど)が変わるたびに、テスト内のモックデータが大量にビルドエラーを起こす。
  • テストケースごとに、どのプロパティが結果に影響しているのか(関心の分離)がブラックボックス化する。

ここで導入すべきベストプラクティスが「Object Factory(テストデータビルダー)パターン」です。

堅牢なテストデータファクトリの実装

テスト用のユーティリティ関数(あるいはヘルパー)を用意し、「デフォルトでは有効な最小限のデータを返しつつ、テストケースごとに必要な部分だけ上書き(Override)できるようにする」のがプロの常道です。

// types.ts (プロダクトの型定義)
type UserProfile = {
id: string;
name: string;
email: string;
role: ‘ADMIN’ | ‘EDITOR’ | ‘VIEWER’;
subscriptionStatus: ‘ACTIVE’ | ‘CANCELED’ | ‘TRIAL’;
lastLoginAt: string;
};

// UserProfileCard.tsx
export const UserProfileCard: React.FC<{ user: UserProfile }> = ({ user }) => {
return (

);
};

さて、このコンポーネントのテストを書くためのファクトリを用意します。

// userFactory.ts (テスト専用のヘルパー)
import { UserProfile } from ‘./types’;

// デフォルトの有効なモックデータを生成するベース関数
export const createMockUser = (override?: Partial): UserProfile => {
return {
id: ‘user-123’,
name: ‘山田 太郎’,
email: ‘taro.yamada@example.com’,
role: ‘VIEWER’,
subscriptionStatus: ‘ACTIVE’,
lastLoginAt: ‘2023-10-01T00:00:00Z’,
…override, // 必要なプロパティだけ外部から上書き可能にする
};
};

このファクトリを使ったテストコードは、驚くほどスッキリします。

// UserProfileCard.test.tsx
import React from ‘react’;
import { render, screen } from ‘@testing-library/react’;
import { UserProfileCard } from ‘./UserProfileCard’;
import { createMockUser } from ‘./userFactory’;

describe(‘UserProfileCard’, () => {
it(‘一般ユーザーの場合、管理者バッジが表示されないこと’, () => {
// デフォルト(role: ‘VIEWER’)のモックをそのまま渡す
const mockUser = createMockUser();

render();

expect(screen.getByText(‘山田 太郎’)).toBeInTheDocument();
expect(screen.queryByTestId(‘admin-badge’)).not.toBeInTheDocument();
});

it(‘管理者ユーザーの場合、管理者バッジが表示されること’, () => {
// 必要なプロパティ(role)だけを上書きしてモックを生成する
const mockAdmin = createMockUser({ role: ‘ADMIN’, name: ‘管理者 花子’ });

render();

expect(screen.getByText(‘管理者 花子’)).toBeInTheDocument();
expect(screen.getByTestId(‘admin-badge’)).toBeInTheDocument();
});
});

このアプローチの強みは、将来的に `UserProfile` 型に `avatarUrl` や `phoneNumber` といった必須プロパティが追加されたとしても、`createMockUser` ファクトリのデフォルト値を1箇所修正するだけで、すべてのテストのビルドエラーを防げる点にあります。これが保守性の高いフロントエンドテストの設計です。

—

3. チルドレン属性(`children`)のモックとコンポーネントの型付け

最後に、コンポーネント合成(Composition)の要である `children` プロップスのテストについて触れておきましょう。

レイアウトコンポーネントやエラーバウンダリ、あるいはデザインシステムのカードコンポーネントなどは、子要素として任意のJSXを受け取ります。

// CardContainer.tsx
import React, { ReactNode } from ‘react’;

type CardContainerProps = {
title: string;
children: ReactNode; // React 18以降の標準的な型
};

export const CardContainer: React.FC = ({ title, children }) => {
return (

{title}

{children}

);
};

チルドレンをテストする際の知見

チルドレンをテストする際は、実際の複雑な子コンポーネントをそのまま渡す必要はありません。テスト対象はあくまで「親としてのレイアウトやラッパーの責務を果たしているか」だからです。ダミーのテキストや、テスト用のモック要素(スタブ)を渡すのが鉄則です。

// CardContainer.test.tsx
import React from ‘react’;
import { render, screen } from ‘@testing-library/react’;
import { CardContainer } from ‘./CardContainer’;

describe(‘CardContainer’, () => {
it(‘指定されたタイトルと、子要素(children)が正しく描画されること’, () => {
render(

テスト用のダミーコンテンツ


);

// タイトルの検証
expect(screen.getByRole(‘heading’, { name: ‘ダッシュボード概要’ })).toBeInTheDocument();

// childrenとして渡した要素が正しくDOMツリーにマウントされているか検証
expect(screen.getByTestId(‘child-content’)).toHaveTextContent(‘テスト用のダミーコンテンツ’);
});
});

ここで `ReactNode` 型をプロップスに指定しているため、文字列、数値、単一のJSX要素、あるいはフラグメントや配列など、Reactがレンダリング可能なすべての型を安全に受け入れることができます。TypeScriptの型安全性を担保しつつ、テストでは柔軟なスタブを流し込む。このバランス感覚がシニアへの第一歩です。

—

シニアからのまとめ

フロントエンドの単体テストは、「動くコードに対して、後からお守りのように書くもの」ではありません。「コンポーネントの仕様書であり、リファクタリングを高速かつ安全に行うための安全網(セーフティネット)」です。

  • コールバックの検証には `jest.fn()` を用い、何がトリガーされるべきかを正確に追跡する。
  • 肥大化したオブジェクトPropsには「Object Factory」を導入し、テストコードのDRY原則と変更耐性を高める。
  • `children` のテストは、本物の複雑な子要素を持ち込まず、最小限のスタブでラッパーの責務をテストする。

これらのプラクティスをチームに定着させれば、あなたのプロダクトのテストスイートは見違えるほど堅牢で、メンテナンスしやすいものに生まれ変わるはずです。さあ、明日からのコードにさっそく取り入れてみてください。

コメント

タイトルとURLをコピーしました