こんにちは!Reactの海へようこそ。チーフアーキテクトの私です。
日々、現場でコードを書いていると、避けて通れないのが「テスト」ですよね。中でも、Componentが画面に出てきた瞬間にゴソゴソと動き出す `useEffect` のテストで、「あれ?なぜかテストが落ちる…」「非同期の処理が終わる前にテストが先に進んじゃうよ!」と、冷や汗をかいた経験はありませんか?
大丈夫ですよ、安心してください。ここを通らずしてReactエンジニアにはなれません。今日は、みんなが一度はつまずく「useEffectのテスト制御」について、身近な例えを交えながら、とことん優しく解き明かしていきましょう!
—
1. なぜ `useEffect` のテストは一筋縄ではいかないのか?
まずは、`useEffect` がやっていることを身近な例えで考えてみましょう。
想像してみてください。あなたは今、近所の「カフェの自動ドア」の前に立っています。
あなたが「近づく(=コンポーネントが画面に現れる)」と、センサーがそれを検知して、自動ドアがウィーーーンと開きますよね(=API通信やタイマー発火などの副作用)。
React Testing Library(RTL)を使ったテストというのは、いわば「ロボットの店員さんが、自動ドアの動作テストを外から覗き見している状態」です。
テストコードはものすごいスピードで上から下に実行されていきます。そのため、ロボット店員はこう叫びがちです。
「あれっ? 今ドアの前に立ったのに、まだドアが開いてないじゃないか!テスト失敗!」
人間なら「少し待てば開くんだな」と分かりますが、機械であるテストは待ってくれません。`useEffect` の中で行われる非同期処理(データの取得など)の完了を、私たちがテスト側できちんとコントロールしてあげる必要があるのです。
—
2. 魔法の杖:`waitFor` と `findBy` の世界
非同期で動く `useEffect` をテストするために、React Testing Libraryには心強い2人の味方がいます。それが `findBy`系メソッド と `waitFor` です。
この2つ、現場でも本当によく使いますが、役割がちょっと違います。
1. `findBy` (例: `findByText`)
- 「お目当ての要素が現れるまで、ちょっと待っててあげるよ(デフォルトで最大1秒)」という優しさを持つクエリです。データ取得系のテストで一番よく使います。
2. `waitFor`
- 「何かが完了するまで、条件が満たされるまで、息を止めて待ち続けるよ」というブロックです。複数の状態変化をまたぐ時や、副作用の完了をじっと見守りたい時に使います。
百聞は一見に如かず。実際に動くコードを見てみましょう!
—
3. 実践!データ取得をするコンポーネントのテスト
よくある「画面が表示されたら、サーバーからユーザー名を取ってきて表示する」というコンポーネントをテストしてみます。
ターゲットのコンポーネント (`UserProfile.jsx`)
import React, { useState, useEffect } from ‘react’;
export function UserProfile({ userId }) {
const [userName, setUserName] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // メモリリーク防止のお手本のような配慮!
async function fetchUserData() {
try {
setLoading(true);
// サーバーからデータを取ってくるフリ(0.1秒かかる想定)
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (isMounted) {
setUserName(data.name);
}
} catch (error) {
console.error(‘データの取得に失敗しました’, error);
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchUserData();
// クリーンアップ関数(コンポーネントが消える時にお片付け)
return () => {
isMounted = false;
};
}, [userId]);
if (loading) {
return
読み込み中…
;
}
return
;
}
このコンポーネント、実務ではすごくよく見かける王道の形ですね。`useEffect` の中でデータを取ってきて、終わったらローディングを消しています。
さあ、これをテストしてみましょう!
—
テストコード (`UserProfile.test.jsx`)
import React from ‘react’;
import { render, screen } from ‘@testing-library/react’;
import { UserProfile } from ‘./UserProfile’;
// 1. fetchを丸ごとモック(偽物)にすり替えます
// 本物のサーバー通信はテストを遅くし、不安定にするのでNGです
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: ‘太郎’ }),
})
);
});
afterEach(() => {
jest.clearAllMocks();
});
test(‘コンポーネントがマウントされると、ユーザー名が正しく表示されること’, async () => {
// 2. コンポーネントを描画する
render(
// 最初は「読み込み中…」が出ているはず(同期的な確認)
expect(screen.getByText(‘読み込み中…’)).toBeInTheDocument();
// 3. 【ここが一番大事!】
// 非同期のデータ取得が終わって、「こんにちは、太郎さん!」が画面に現れるのを待つ!
// `findByText` を使うことで、表示されるまでテストが自動で待ってくれます。
const welcomeMessage = await screen.findByText(‘こんにちは、太郎さん!’);
// 4. 無事に表示されたことを確認!
expect(welcomeMessage).toBeInTheDocument();
// ついでに「読み込み中…」が消えていることも確認しておくと完璧ですね
expect(screen.queryByText(‘読み込み中…’)).not.toBeInTheDocument();
});
コードの解説:なぜうまくいったのか?
ここで注目してほしいのは、テスト関数の冒頭にある `async` と、中の `await screen.findByText(…)` です。
JavaScriptは、`fetch` や `useEffect` の非同期処理を待たずに次の行へ進もうとします。そこで `await` を使うことで、テスト側に「ねえ、太郎さんの名前が画面に出るまで、ちょっとここで待っていてね」と指示を出しているのです。
もしここで `findBy` ではなく `getByText` を使ってしまうと、「あれっ、まだデータ取れてないじゃん!」とテストが即座に怒り出し、テストが失敗(Red)してしまいます。ここが、初心者が一番ハマりやすい罠なんです。
—
4. チーフアーキテクトからの実践アドバイス
最後に、現場で「おっ、やるな!」と思われるための大切なマントラをいくつか授けましょう。
1. 副作用(NetworkやTimer)は、必ずモック(偽物)にしよう
- テストの中で本物のAPIを叩いてはいけません。ネットワークのエラーや速度にテストが左右されてしまい、いわゆる「フレークテスト(まぐれで落ちたり通ったりするテスト)」の温床になります。`jest.fn()` や `MSW (Mock Service Worker)` を使いこなしましょう。
2. 「状態の変化」の順序を意識する
- 「ローディング中が出る」→「非同期処理が終わる」→「データが表示される」という、コンポーネントのライフサイクルのストーリーを頭に思い浮かべながらテストを組み立てると、迷いにくくなります。
3. エラーのケースもテストしよう
- 今回は成功パターンでしたが、「APIがエラーを返した時に、エラーメッセージがきちんと画面に出るか」という `catch` 側のテストも、`waitFor` を使えば同じように美しく書くことができますよ。
最初は難しく感じるテストの世界ですが、一つひとつ仕組みを紐解いていけば、あなたのコードの強力な「お守り」になってくれます。
焦らず、一歩ずつ進んでいきましょう。あなたのReactライフが素晴らしいものになるよう、いつも応援しています!

コメント