はじめに
React Testing Libraryを使ったテストで、複数のエラーが発生しました。原因はテストのセットアップ方法と非同期処理の扱いに問題があったためです。この記事では、エラーの内容と解決方法を記録します。
エラー内容
エラー1: 二重レンダリングによる複数要素の検出
エラーメッセージ:
Found multiple elements with the placeholder text of: 学習内容を入力
詳細:
テストコードとヘルパー関数の両方でrender(<App />)を呼び出していたため、同じコンポーネントが2回レンダリングされていました。
// sample.spec.js
it("登録ボタンをクリックしたら...", async () => {
render(<App />); // ← 1回目のrender
const { titleInput, ... } = await setup(); // ← setup内で2回目のrender
});
// setup.js
export const setup = async () => {
render(<App />); // ← これが2回目のrender
await screen.findByText("test");
return { ... };
};
結果として、同じplaceholderを持つinput要素が2つ存在し、findByPlaceholderTextがエラーを返しました。
エラー2: 要素が見つからない(非同期処理の待機漏れ)
エラーメッセージ:
Unable to find an element by: [data-testid="title"]
詳細:
登録ボタンをクリックした後、非同期処理の完了を待たずに削除ボタンをクリックしていたため、期待した状態になりませんでした。
// 問題のあるコード
it("学習記録を削除したら要素が一つ減る", async () => {
const { titleInput, timeInput, registerButton, deleteButtons } = await setup();
fillForm(titleInput, timeInput, "test3", "8");
fireEvent.click(registerButton); // 非同期で登録処理が開始
fireEvent.click(deleteButtons[0]); // ← 登録完了を待たずに削除を実行!
// ...
});
さらに、deleteButtonsはsetup()時点で取得した古い参照だったため、登録後に追加された新しい削除ボタンを操作できませんでした。
根本原因:非同期処理と要素の参照
React Testing Libraryでは、非同期処理(API呼び出しなど)の完了後にDOMが更新されます。以下の点に注意が必要です:
-
非同期処理は
await waitFor()で待つ:状態変更が完了するまで待機する - 要素の参照は操作直前に取得する:DOM更新後に古い参照を使うと、存在しない要素を操作してしまう
[NG] setup()で取得 → 登録 → 古いdeleteButtonsで削除
[OK] setup()で取得 → 登録 → waitFor()で待機 → 新しくdeleteButtonsを取得 → 削除
エラー解決
修正1: 二重レンダリングの解消
テストコードからrender(<App />)を削除し、setup()関数にレンダリングを任せるように統一しました。
Before:
// sample.spec.js
it("登録ボタンをクリックしたら...", async () => {
render(<App />); // ← 削除
const { titleInput, ... } = await setup();
});
After:
// sample.spec.js
it("登録ボタンをクリックしたら...", async () => {
const { titleInput, ... } = await setup(); // setup()内でrenderが行われる
});
修正2: 非同期処理の待機と要素の再取得
登録処理の完了を待ってから、削除ボタンを操作直前に取得し直すように修正しました。
Before:
it("学習記録を削除したら要素が一つ減る", async () => {
const { titleInput, timeInput, registerButton, deleteButtons } = await setup();
fillForm(titleInput, timeInput, "test3", "8");
fireEvent.click(registerButton);
fireEvent.click(deleteButtons[0]); // 古い参照を使用
// ...
});
After:
it("学習記録を削除したら要素が一つ減る", async () => {
const { titleInput, timeInput, registerButton } = await setup();
fillForm(titleInput, timeInput, "test3", "8");
fireEvent.click(registerButton);
// 登録が完了するのを待つ
await waitFor(() => {
expect(screen.getAllByTestId("title")).toHaveLength(2);
});
// 操作直前に削除ボタンを取得
const deleteButtons = screen.getAllByRole("button", { name: "削除" });
fireEvent.click(deleteButtons[0]);
// ...
});
改善ポイントまとめ
| 問題 | 原因 | 解決策 |
|---|---|---|
| 複数要素が見つかる | 二重レンダリング |
render()は1箇所に統一 |
| 要素が見つからない | 非同期処理の待機漏れ |
await waitFor()で状態変更を待つ |
| 操作が効かない | 古い要素参照の使用 | 操作直前に要素を再取得する |
おわりに
実装・テストともに要素の取得のタイミングが大切であることを再認識しました。
またリファクタリングをすることで今までの作業がすべて腹落ちしたと思います。
やっぱり、知識や技術をつけるにはリファクタリングはいいタスクになると実感しました。
React Testing Libraryでテストを書く際は、以下の点を意識することが重要です:
- レンダリングは1回だけ:ヘルパー関数を使う場合は、どこでレンダリングするか統一する
-
非同期処理は必ず待つ:
waitFor()やfindBy*を使って状態変更の完了を確認する - 要素は新鮮な状態で取得:DOM更新後に操作する要素は、操作直前に取得し直す
学び: テストコードも本番コードと同様に「状態の変化」を意識し、適切なタイミングで待機・取得を行う必要がある。