なぜ `fireEvent` を卒業すべきなのか? ― `@testing-library/user-event` で手に入れる「本物のユーザー体験」
Reactのテストを書き始めたばかりの頃、誰もが一度は通る道が `fireEvent` です。「とりあえずボタンを押したことにしたいから `fireEvent.click()` を呼ぶ」。その選択、実は少しだけ「テストの信頼性」を損なっているかもしれません。
今日は、なぜ僕たちが実務で `user-event` を強く推奨するのか、そしてブラウザの裏側で何が起きているのかを深掘りしつつ、明日から使える実装パターンを伝授します。
—
1. `fireEvent` と `user-event` の決定的な境界線
端的に言うと、`fireEvent` は「DOMイベントを発火させるだけ」ですが、`user-event` は「ユーザーの一連の操作をシミュレートする」ライブラリです。
`fireEvent.click()` を叩いたとき、Reactのイベントハンドラは即座に実行されます。しかし、実際のブラウザでユーザーがボタンをクリックするとき、そこには「マウスダウン → フォーカス → マウスアップ → クリック」という複数のイベントの連鎖が存在します。
もし、あなたのコンポーネントが `onFocus` や `onMouseDown` に依存した複雑な状態管理をしていたら? `fireEvent` ではその依存関係をすり抜けてしまい、「テストは通るのに、本番では動かない」という、エンジニアにとって最も胃が痛くなるバグを生むことになります。
2. 実践:洗練されたテストコードの書き方
`@testing-library/user-event` を導入する際は、必ず `setup()` 関数を使ってインスタンスを生成してください。これにより、テストごとの状態(フォーカス位置など)をクリーンに保てます。
import { render, screen } from ‘@testing-library/react’;
import userEvent from ‘@testing-library/user-event’;
import MyForm from ‘./MyForm’;
test(‘ユーザー操作を完璧にシミュレートする’, async () => {
// 1. setupでインスタンスを生成(ここがベストプラクティス)
const user = userEvent.setup();
render(
const input = screen.getByRole(‘textbox’, { name: /ユーザー名/i });
const button = screen.getByRole(‘button’, { name: /送信/i });
// 2. タイピングのシミュレーション
// typeは「フォーカス→キーダウン→キープレス→キーアップ」を一連の流れとして実行します
await user.type(input, ‘Hello World’);
// 3. クリック操作
// これにより、クリックに関連する全てのイベントが自然な順序で発火します
await user.click(button);
// 4. アサーション
expect(input).toHaveValue(‘Hello World’);
});
3. なぜ `await` が必要なのか?(ここが重要)
`fireEvent` は同期的に動きますが、`user-event` のメソッドはすべて `Promise` を返します。なぜなら、ブラウザのイベントループをまたいで、より自然なタイミングで操作を反映させる設計になっているからです。
現場でよくあるのが、`await` を忘れて「テストが先に終了してしまい、ステート更新が反映されない」というケースです。`userEvent` を使ったら、基本は全て `await` すると心に刻んでください。
4. 現場で役立つ Tips:フォーカスとキーボード操作
実務では、単なるクリックよりも「フォームのバリデーション」や「キーボードナビゲーション」が鬼門になります。特に `tab` キーでの移動をテストしたい場合、`user-event` の真価が発揮されます。
test(‘Tabキーによるフォーカス移動とEnter送信’, async () => {
const user = userEvent.setup();
render(
// tab()を呼ぶことで、ブラウザのフォーカス遷移をシミュレート
await user.tab();
expect(screen.getByRole(‘textbox’)).toHaveFocus();
// キーボード入力を直接シミュレート
await user.keyboard(‘{Enter}’);
// 送信後の挙動を確認…
});
最後に:テストは「コードの保険」ではなく「仕様の証明」
僕が後輩にいつも伝えているのは、「テストコードは動けばいいというものではない」ということです。
`fireEvent` を使うことは、ブラウザの挙動をハックして強引にイベントをねじ込む行為に近いです。一方で `user-event` を使うことは、「ユーザーがどうやってこのコンポーネントを操作するのか」をドキュメントとして残す行為です。
テストコードを読んだときに、実際にユーザーが画面の前でどう動いているかが頭に浮かぶ。それこそが、メンテナンス性の高いフロントエンド開発の第一歩です。
まずは今のプロジェクトの `fireEvent` を一つだけ `userEvent.click()` に置き換えてみてください。テストの信頼性が少しずつ、しかし確実に向上していく感覚を味わえるはずです。何か詰まったら、いつでも聞いてください。一緒に最高のコードを書きましょう。

コメント