はじめに
フォームの必須チェックを React Hook Form + Chakra UI で実装し、Testing Library(Jest) でテストしたら、UIでは見えているエラーメッセージがテストでは見つからない——そんな経験、ありませんか?
この現象の多くは「非同期描画」と「テキストの一致条件」が原因です。ここでは、同じハマりを避けるための汎用的な原因と解決策を、最小コードとテスト例でシンプルにまとめます。
問題
テストが落ちる:
Unable to find an element with the text: IDは必須です
落ちるテスト例:
すぐにDOMを探してしまう
expect(screen.getByText("IDは必須です")).toBeInTheDocument();
なぜ起きる?(普遍的な原因)
-
非同期描画
React Hook Form のバリデーションは submit/blur 後に state が更新 → DOM に反映される。クリック直後はまだ表示されない。 -
条件付きレンダリング
Chakra UIの<FormErrorMessage>
はエラー時だけ描画。エラーが無ければ DOM に存在しない。 -
テキスト一致の厳しさ
完全一致だと<p>
などのラップや微妙な文言差で拾えないことがある。
解決方法(まずはこれで安定)
- 非同期待機:findBy/ waitFor を使う
要素が現れるまで待つ
expect(await screen.findByText(/IDは必須です/i)).toBeInTheDocument();
もしくは
await waitFor(() => {
expect(screen.getByText(/IDは必須です/i)).toBeInTheDocument();
});
2.テキストは柔らかくマッチ
正規表現(/…/i)で大文字小文字や微妙な差に強くする
UIとテストで文言を統一(「必須です」 vs 「IDは必須です」などのズレをなくす)
3.できれば userEvent を使う
実際のユーザー操作に近いイベント(type, click, tab, clear など)を発火することで、blur/change 等が自然に起き、再現性が高いテストになる。
const user = userEvent.setup();
await user.type(screen.getByLabelText(/ID/i), "");
await user.click(screen.getByRole("button", { name: /登録/i }));
expect(await screen.findByText(/IDは必須です/i)).toBeInTheDocument();
コード全体(最小例)
コンポーネント
<FormControl isInvalid={!!errors.userId}>
<FormLabel>ID</FormLabel>
<Input {...register("userId", { required: "IDは必須です" })} />
<FormErrorMessage>{errors.userId?.message}</FormErrorMessage>
</FormControl>
テスト
test("ID未入力でエラー表示", async () => {
render(<SampleForm />);
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: /登録/i }));
expect(await screen.findByText(/IDは必須です/)).toBeInTheDocument();
});
まとめ
-
「UIでは見えるのにテストで取れない」= 非同期描画が原因
-
findByText / waitFor を使い、DOMに現れるまで待つ