【入門編】 React Testing LibraryでのuseEffectのテスト制御 – React実践ガイド

こんにちは!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

こんにちは、{userName}さん!

;
}

このコンポーネント、実務ではすごくよく見かける王道の形ですね。`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ライフが素晴らしいものになるよう、いつも応援しています!

コメント

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