やあ、今日もコードと格闘ご苦労様。
中級の壁を突破しようともがいている君なら、そろそろ「動くコードを書く」フェーズから、「保守しやすく、テストでガチガチに守られた堅牢なコンポーネントを設計する」フェーズへ移行したい頃合いだと思う。
今回は、実務で毎日と言っていいほどお世話になる「React Testing Library(RTL)を用いたPropsの検証」について、現場のリアルな知見を交えて徹底的に解説していこう。
巷にあふれる「とりあえず `screen.getByText` 使っとけ」みたいな浅い解説はもう卒業だ。ブラウザの裏側の動きや、なぜそのクエリを選択すべきなのかという哲学まで含めて、しっかりと腹に落としていってほしい。
—
なぜPropsのテストで「実装」ではなく「出力」を検証するのか
僕たちがコンポーネントを書くとき、親から子へ様々なデータをPropsとして流し込む。
「このテキストが表示されているか」「このボタンをクリックしたときに渡されたコールバックが発火するか」——これらはすべて、ユーザーから見た「振る舞い」そのものだ。
RTLの最大の哲学は、「実装の詳細をテストするな、ユーザーの体験をテストせよ」という点にある。
useStateの内部変数や、コンポーネント内部のプライベート関数をテストしようとする人がたまにいるが、それはアンチパターンだ。内部構造をリファクタリングした瞬間にテストが壊れる、いわゆる「脆弱なテスト(Brittle Tests)」の出来上がりだからね。
僕たちが検証すべきなのは、「特定のPropsが渡されたとき、DOMツリーがどう変化し、ブラウザ上でどう振る舞うか」の1点に尽きる。
—
裏側の処理:RTLがDOMとやり取りしていること
テストコードで `render(
RTLは、JestやVitestといったテストランナーの環境下に `jsdom` というヘッドレスなDOM環境を立ち上げ、そこにReactのコンポーネントをマウント(`ReactDOM.render` 相当の処理)している。
つまり、ブラウザがHTMLを解釈してレンダリングするプロセスを、メモリ上で軽量にシミュレートしているんだ。
ここで重要になるのが、クエリの選び方だ。
- `getByRole`: スクリーンリーダーなどのアクセシビリティツリーを基準に要素を探す。最も実ユーザーの体験に近い。
- `getByText`: テキストの内容で探す。静的なラベルやメッセージの検証に強い。
- `queryByText`: 要素が「存在しないこと」を検証したいときに使う(`getBy` だと要素がない場合に即座にテストが落ちるため)。
このあたりの使い分けをミスると、意味のない「グリーン(成功する)だけど保守性の低いテスト」が量産されることになる。
—
実践:現場で即戦力になるProps検証パターン
百聞は一見に如かず。実務でそのまま使える、TypeScriptとRTLを用いた堅牢なコンポーネントとテストコードのセットを見ていこう。
今回は、ユーザー情報を受け取り、権限に応じて表示を変え、クリックイベントをハンドリングする `UserProfileCard` というコンポーネントを題材にする。
1. コンポーネントの実装 (`UserProfileCard.tsx`)
import React from ‘react’;
// チルドレン属性や各種Propsの型定義(実務では別ファイルに切り出すことも多いね)
export interface UserProfileCardProps {
/ ユーザーの表示名 /
name: string;
/ ユーザーのメールアドレス /
email: string;
/ 管理者権限を持っているか /
isAdmin?: boolean;
/ カードクリック時のコールバック /
onCardClick?: () => void;
/ 拡張用のチルドレン(バッジや追加情報など) /
children?: React.ReactNode;
}
export const UserProfileCard: React.FC
name,
email,
isAdmin = false,
onCardClick,
children,
}) => {
return (
{name}
{email}
{/ 条件付きレンダリングのProps検証でよく使われるパターン /}
{isAdmin && 管理者}
{/ チルドレンの受け渡し /}
);
};
2. テストコードの実装 (`UserProfileCard.test.tsx`)
さて、ここからが本番だ。上記のコンポーネントに対して、Propsの受け渡しが正しくDOMに反映されているかをテストするコードを書くよ。
import React from ‘react’;
import { render, screen } from ‘@testing-library/react’;
import userEvent from ‘@testing-library/user-event’;
import { UserProfileCard } from ‘./UserProfileCard’;
describe(‘UserProfileCard コンポーネントのProps検証’, { name: ‘UserProfileCard’ }, () => {
test(‘基本Props(name, email)が正しくレンダリングされること’, () => {
// 1. 準備 (Arrange): 必須のPropsを渡してレンダリング
render(
// 2. 実行 & 検証 (Act & Assert): getByRoleとgetByTextでDOMを探索
// アクセシビリティの観点から region ロールと aria-label で大枠を捉える
const region = screen.getByRole(‘region’, { name: ‘山田 太郎のプロフィール’ });
expect(region).toBeInTheDocument();
// テキストが意図通りにDOMに現れているか
expect(screen.getByRole(‘heading’, { name: ‘山田 太郎’ })).toBeInTheDocument();
expect(screen.getByText(‘yamada@example.com’)).toBeInTheDocument();
});
test(‘isAdminがtrueの場合のみ管理者バッジが表示され、falseの場合は存在しないこと’, () => {
// 3. パターンA: isAdmin = true の場合
const { rerender } = render(
);
// queryByTestId または queryByText を使用(存在しないことを検証するため)
expect(screen.getByTestId(‘admin-badge’)).toBeInTheDocument();
// 4. パターンB: 同じコンポーネントインスタンスでPropsをfalseに差し替える検証 (rerenderの活用)
rerender(
);
// queryBy系は要素が見つからない場合 null を返すため、not.toBeInTheDocument() と組み合わせる
expect(screen.queryByTestId(‘admin-badge’)).not.toBeInTheDocument();
});
test(‘onCardClickが渡された場合、クリックイベントが正常に発火すること’, async () => {
// モック関数を用意(VitestやJestの機能)
const handleClick = jest.fn(); // Vitestなら vi.fn()
render(
);
// ユーザーの実際の操作をシミュレートするライブラリ userEvent を使用
const card = screen.getByRole(‘region’, { name: ‘テスト ユーザーのプロフィール’ });
await userEvent.click(card);
// コールバックが1回呼ばれたことを検証
expect(handleClick).toHaveBeenCalledTimes(1);
});
test(‘childrenとして渡された要素が正しく内部に描画されること’, () => {
render(
);
// 親から流し込まれた子要素(children)が正しくレンダリングされているか
expect(screen.getByRole(‘button’, { name: ‘カスタムアクション’ })).toBeInTheDocument();
});
});
—
シニアからの実践アドバイス:テストを書くときの極意
ここまで読んでくれた君に、実務で絶対に知っておいてほしい「現場の知恵」をいくつか授けよう。
1. `getBy` と `querygetBy` の使い分けを間違えるな
要素が「存在すること」を確かめたいときは `getBy…` を使え。要素がない場合にテストが親切にエラーを吐いてくれる。逆に、条件付きレンダリングなどで「今は存在してはいけないこと」を証明したいときだけ `queryBy…` を使い、`not.toBeInTheDocument()` でアサーションをかけるんだ。これを逆にすると、バグを見逃す温床になる。
2. `fireEvent` ではなく `userEvent` を使え
昔は `fireEvent.click()` が主流だったが、今のモダンなReactテストでは `@testing-library/user-event` がデファクトだ。`userEvent` は、ユーザーが実際にマウスを動かしてフォーカスし、クリックするという一連のブラウザのネイティブな挙動をより忠実に再現してくれる。非同期処理になることが多いので `await` を忘れないようにしよう。
3. 型安全性(TypeScript)をテストの味方にしろ
Propsのテストを書くとき、存在しないPropsをテストしようとしてTypeScriptのコンパイルエラーになることがある。それは素晴らしいことだ。テストコード自体も厳格な型チェックの網羅性の中に組み込むことで、リファクタリングにビクともしない要塞のようなコードベースが完成する。
—
おわりに
Propsの検証は、一見地味で単調な作業に見えるかもしれない。
しかし、ここを妥協せずにしっかりとテストを書くエンジニアと、感覚だけでコードを書くエンジニアの間には、数ヶ月後、数年後にコードベースの健康状態として圧倒的な差となって現れる。
君が書いたそのテストは、未来のチームメンバー(あるいは数ヶ月後の自分自身)が自信を持ってコードを改修するための、最高のセーフティネットになるんだ。
さあ、エディタに戻って、今日のコードに堅牢なテストを組み込んでやろう。質問があればいつでも僕のところへ来るといい。健闘を祈る!

コメント