useEffectのテストは「戦場」だ。だからこそ、制御術を極めろ。
フロントエンドの現場で、「テストが不安定(Flaky)だ」と嘆く声を聞くたびに、私はいつも思うんだ。「それはReactの非同期レンダリングと、useEffectの気まぐれな実行タイミングを理解しきれていない証拠だよ」と。
`useEffect`は便利だ。だが、テストという限定された環境下では、ブラウザの描画パイプラインから切り離された「制御不能な暴れ馬」になりかねない。今日は、React Testing Library(RTL)を使って、この暴れ馬をどう手懐けるか、現場の視点から深掘りしていこう。
—
なぜuseEffectのテストは「闇」なのか
ブラウザの裏側を思い出してほしい。Reactはレンダーフェーズの後、コミットフェーズを経てDOMを更新し、その後に`useEffect`をスケジュールする。つまり、テスト環境(jsdom)においても、`render()`を呼んだ直後には、副作用はまだ完了していないことがほとんどだ。
多くの駆け出しエンジニアが陥る罠は、`render`した直後に`expect`を書いてしまうことだ。「なぜデータが空なんだ?」「なぜAPIが叩かれていないんだ?」と悩む時間は、もう終わりにしよう。
—
実践:副作用を「待つ」のではなく「追い越す」
RTLの鉄則は、「実装の詳細をテストするな」だ。`useEffect`の中で何をしているか(内部実装)ではなく、副作用の結果として「DOMがどう変化したか」を監視するのが正攻法だ。
ここで最強の武器になるのが `findBy` クエリと `waitFor` だ。
1. APIコールを伴うuseEffectのテスト例
例えば、マウント時にユーザー情報を取得するコンポーネントをテストするとしよう。
import { render, screen, waitFor } from ‘@testing-library/react’;
import UserProfile from ‘./UserProfile’;
import as api from ‘./api’;
// モック化:これがテストの第一歩だ
jest.mock(‘./api’);
test(‘コンポーネントマウント時にユーザーデータを取得・表示する’, async () => {
const mockUser = { name: ‘React 伝説のアーキテクト’ };
(api.fetchUser as jest.Mock).mockResolvedValueOnce(mockUser);
render(
// 1. ローディング状態を確認(副作用が走る前のDOM)
expect(screen.getByText(/読み込み中/i)).toBeInTheDocument();
// 2. findByで副作用完了後のDOMが出現するのを待つ
// findBy系は内部でwaitForを使っている。これが最も「Reactらしい」待機方法だ。
const userName = await screen.findByText(mockUser.name);
expect(userName).toBeInTheDocument();
// 3. APIが正しく呼ばれたか確認
expect(api.fetchUser).toHaveBeenCalledTimes(1);
});
—
依存配列の「再実行」を制御するテクニック
実務でよくあるのが、「propsが変わった時にだけ実行したいのに、テストでどう再現すればいいかわからない」という悩みだ。
Reactのテストでpropsを更新するには、`rerender`関数を使う。ここでも、副作用が走った後のDOM状態を正しく待機(Await)させることが肝だ。
test(‘propsの変更に応じて副作用が再発火する’, async () => {
const { rerender } = render(
// 最初の取得を待つ
await screen.findByText(/User 1/);
// propsを変えて再レンダリングを強制
rerender(
// ここでwaitForを使う。findByでも良いが、DOMの変化が複雑な場合はこちらが柔軟だ
await waitFor(() => {
expect(screen.getByText(/User 2/)).toBeInTheDocument();
});
});
—
プロからのアドバイス:それでもテストが落ちる君へ
もし`waitFor`を多用してもテストが落ちるなら、それはテストの書き方の問題ではない。「コンポーネントの設計」に無理があるサインだ。
1. 副作用が複雑すぎないか?
- `useEffect`の中にロジックを詰め込みすぎていないか。カスタムフックに切り出して、フック単体でテスト(`@testing-library/react-hooks`)できないか検討しろ。
2. 依存配列は正しく管理されているか?
- `eslint-plugin-react-hooks`の警告を無視していないか。これが守られていないテストは、ただの「運ゲー」だ。
3. クリーンアップ関数は書いたか?
- APIリクエスト中にコンポーネントがアンマウントされたらどうなる?メモリリークの温床になるし、テストでも予期せぬエラーを吐く。必ず`AbortController`等でリクエストをキャンセルする処理を入れろ。
最後に
テストを書くということは、コンポーネントの「呼吸」を理解することだ。Reactがいつレンダリングし、いつ副作用を吐き出し、いつクリーンアップするか。そのリズムを指先で感じられるようになれば、君はもう中級者を超えたスペシャリストの入り口に立っている。
泥臭いデバッグも、テストコードが緑色(Pass)に変わる瞬間の快感があれば乗り越えられるはずだ。さあ、エディタを開いて、自信を持って副作用を制御してくれ。健闘を祈る。

コメント