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

こんにちは!Reactの世界へようこそ、チーフアーキテクトの私です。

Reactを学び始めて、「画面を作るのは楽しくなってきたぞ!」という頃に、多くの人がぶつかる最初の高い壁……それがそう、「単体テスト(Jestなどを使ったテスト)」ですよね。

「コンポーネントのテストを書きましょう」と言われてテストファイルを開いたものの、画面に必要なデータ(Props)をどうやって渡せばいいのか、ボタンを押したときに動くはずの関数(コールバック)はどう偽装(モック化)すればいいのか……。画面いっぱいに広がる赤色のエラーログを見て、「もう無理!」とそっとブラウザのタブを閉じたくなる気持ち、痛いほどよく分かります。

でも、大丈夫ですよ。安心してください。
今日は、複雑に見えるPropsのモック化を、身近な「お買い物」のたとえ話を交えながら、一緒に優しく解きほぐしていきましょう!

—

そもそも「Propsのモック化」ってなに?

テストにおける「モック(Mock)」とは、一言でいうと「本物そっくりの代役(おもちゃ)」のことです。

たとえば、あなたが新しいおもちゃのレジスターを買ったとします。本物のスーパーマーケットみたいに、本物のクレジットカードでお金を決済してテストするわけにはいきませんよね?おもちゃのプラスチックのカードや、音だけ鳴る偽物のコインを使って、「ちゃんとレジとして動くかな?」と安全に動作を確かめるはずです。

Reactのコンポーネントテストもこれと全く同じです。
コンポーネントが外から受け取るデータ(Props)や、親 componente に伝えるための関数(コールバック)の本物は、ときにはデータベースと通信したり、複雑な処理をしたりしてテストしにくいもの。だからこそ、「テスト用の都合の良いダミーデータや、偽物の関数(`jest.fn()`)」をこっそり差し込んで、テストをスムーズに行うんです。

—

身近な例え:おもちゃの「お買い物ボタン」で考えてみよう

想像してみてください。ここに、「カートに入れる」ボタンを持つ『商品カード』というコンポーネントがあります。

このコンポーネントは、お仕事として以下の2つを必要としています。
1. 商品の情報(名前や価格)というデータ(オブジェクトのProps)
2. 「ボタンが押されたよ!」と親に伝える連絡網(コールバック関数のProps)

これをテストするときに、本物のデータベースや親の複雑な処理を持ち出す必要はありません。「ダミーの商品データ」と「ボタンが押されたかを記録するスパイ(`jest.fn()`)」を用意してあげれば十分なんです。

実際のコードを見てみましょう。

1. テストするコンポーネントの例

まずは、テスト対象となるシンプルなコンポーネントです。

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

// Propsの型定義(お仕事の仕様書みたいなものです)
type ProductProps = {
product: {
id: number;
name: string;
price: number;
description: string; // テストでは使わないかもしれない詳細情報
imageUrl: string; // これも今回はテストには不要かも…
};
onAddToCart: (productId: number) => void; // ボタンを押したときに呼ばれる関数
};

export const ProductCard: React.FC = ({ product, onAddToCart }) => {
return (

{product.name}

価格: {product.price}円

{/ ボタンが押されたら、親から預かった関数を呼び出す /}

);
};

—

2. 複雑なオブジェクトPropsを「簡略化」して渡すコツ

ここで注目してほしいのが、`product` オブジェクトの中身です。本番コードでは、画像URLや詳細な説明文など、たくさんのプロパティが入っているかもしれません。

しかし、テストのときは「今、何をテストしたいのか」に集中することが大切です。今回の主役は「名前と価格が正しく表示されるか」と「ボタンを押したら関数が呼ばれるか」だけ。

そのため、テスト用のデータは必要最低限(ミニマム)に削ぎ落として(モック化して)渡してあげましょう。

3. `jest.fn()` でコールバック関数をモック化する

「ボタンが押されたときに関数がちゃんと動いたか?」を確かめるために、Jestが用意してくれた魔法の道具が `jest.fn()` です。これは、「呼ばれた回数や、渡された引数をこっそりノートにメモしてくれる優秀なスパイ(お助けマン)」だと思ってください。

それでは、実際のテストコードを見てみましょう。

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

describe(‘ProductCard コンポーネントのテスト’, () => {

// テストケース1:商品情報が正しく画面に表示されているか?
test(‘商品の名前と価格が正しく表示されること’, () => {
// 1. 【モックデータの準備】
// テストに最低限必要なデータだけを、スリムに用意します!
const mockProduct = {
id: 1,
name: ‘美味しいりんご’,
price: 150,
description: ‘テストでは使わないので空っぽでもOK’,
imageUrl: ”,
};

// 2. 【コールバックのモック準備】
// まだ中身のない、形だけの「メモ魔の関数」を作ります
const mockAddToCart = jest.fn();

// 3. 【コンポーネントのレンダリング(画面への描画)】
render();

// 4. 【アサーション(検証)】
// 画面に「美味しいりんご」と「150円」が表示されているかチェック!
expect(screen.getByText(‘美味しいりんご’)).toBeInTheDocument();
expect(screen.getByText(‘価格: 150円’)).toBeInTheDocument();
});

// テストケース2:ボタンを押したときに、コールバック関数が正しく呼ばれるか?
test(‘「カートに入れる」ボタンを押すと、商品のIDを添えて関数が呼ばれること’, async () => {
// 準備:今回はIDだけに注目します
const mockProduct = {
id: 99, // テスト用の特別なID
name: ‘みかん’,
price: 100,
description: ”,
imageUrl: ”,
};

// スパイ機能を持ったモック関数を用意
const mockAddToCart = jest.fn();

render();

// ユーザーの「ボタンをクリックする」というアクションを再現
const button = screen.getByRole(‘button’, { name: ‘カートに入れる’ });
await userEvent.click(button);

// 【ここがポイント!】
// メモ魔のモック関数(mockAddToCart)が「1回だけ呼ばれたか?」をチェック
expect(mockAddToCart).toHaveBeenCalledTimes(1);

// さらに、「正しい商品のID(99)が引数として渡されて呼ばれたか?」まで検証!
expect(mockAddToCart).toHaveBeenCalledWith(99);
});
});

—

現場で役立つ!チーフアーキテクトからのワンポイントアドバイス

ここまで読んでくれてありがとうございます。最後に、実務の現場でテストを書くときに心がけてほしい大切なマインドを2つだけお伝えしますね。

1. テスト用のオブジェクトは使い回せる(共通化の罠に注意)
テストファイルが長くなると、「ダミーデータを一箇所の変数にまとめて使い回そう」としがちです。ですが、あるテストでそのデータをうっかり書き換えてしまい、別のテストが謎のエラーを吐く……という「テストのバグ」にハマりがちです。テストデータは、できるだけ各テストケースの中でシンプルに定義するか、ヘルパー関数を使うのが安全です。

2. 「すべてを再現しよう」としない
本番の巨大なデータ構造(TypeScriptの型)をそのままテストで再現しようとすると、それだけでテストコードが真っ赤なエラーに染まってしまいます。「今回のテストで本当に必要なプロパティはどれだっけ?」と引き算の視点を持つことが、テストを書きやすく、そして読みやすくする最大の秘訣です。

最初の一歩は難しく感じるかもしれませんが、`jest.fn()` というお助けマンと、ミニマムなモックデータの作り方さえ分かってしまえば、テストは驚くほど楽しく、そしてあなたの心強い味方になってくれます。

焦らず、一歩ずつ自分のペースで進めていきましょう。応援しています!

コメント

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