はじめに
テストを書いていて、fireEventとuserEventがあり似たような使い方をするなと思って、違いはなんだろうと思い、調べたのでまとめます。
fireEvent
公式より
Fire DOM events.
DOM イベントを発生させます。
ただ、実際のユーザーが入力する際のマウスダウン、フォーカス、入力、マウスアップといった連続したイベントを完全に再現できない場合があるようです。
import React, { useState } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
function SampleInput() {
const [value, setValue] = useState('');
return (
<>
<label htmlFor="name">Name</label>
<input
id="name"
aria-label="Name"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p data-testid="value">{value}</p>
</>
);
}
test('fireEvent.change で値を変更できる', () => {
render(<SampleInput />);
const input = screen.getByLabelText('Name');
// change イベントを「単発」で発火
fireEvent.change(input, { target: { value: 'Taro' } });
expect(screen.getByTestId('value')).toHaveTextContent('Taro');
});
userEvent
ユーザーの実際の操作をシミュレートする関数です。
import React, { useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function SampleInput() {
const [value, setValue] = useState('');
return (
<>
<label htmlFor="name">Name</label>
<input
id="name"
aria-label="Name"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p data-testid="value">{value}</p>
</>
);
}
test('userEvent.type で実入力に近い形で値を入れられる', async () => {
const user = userEvent.setup();
render(<SampleInput />);
const input = screen.getByLabelText('Name');
// クリック → フォーカス → キー入力… のような流れに近い
await user.type(input, 'Taro');
expect(screen.getByTestId('value')).toHaveTextContent('Taro');
});
どっちがいいのか
公式の見解は
Most projects have a few use cases for fireEvent, but the majority of the time you should probably use @testing-library/user-event.
ほとんどのプロジェクトでは fireEvent のユースケースは数件程度ですが、大半の場合は @testing-library/user-event を使用すべきでしょう。
user-even(userEvent)が良さそうですね。
実際のコードでの違いは?
fireEvent
import React, { useState } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
function RegisterForm() {
const [userId, setUserId] = useState('');
const [touched, setTouched] = useState(false);
return (
<>
<label htmlFor="userId">ユーザーID</label>
<input
id="userId"
aria-label="ユーザーID"
value={userId}
onChange={(e) => setUserId(e.target.value)}
onBlur={() => setTouched(true)}
/>
{touched && userId === '' && (
<p role="alert" data-testid="error">
ユーザーIDは必須です
</p>
)}
<button type="button">登録</button>
</>
);
}
test('fireEvent だとフォーカス/blur を明示的に積むことが多い', () => {
render(<RegisterForm />);
const input = screen.getByLabelText('ユーザーID');
// fireEvent は「必要なイベントを自分で積む」書き方になりやすい
fireEvent.focus(input);
fireEvent.change(input, { target: { value: '' } });
fireEvent.blur(input);
expect(screen.getByTestId('error')).toHaveTextContent('ユーザーIDは必須です');
});
user-even(userEvent)
import React, { useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function RegisterForm() {
const [userId, setUserId] = useState('');
const [touched, setTouched] = useState(false);
return (
<>
<label htmlFor="userId">ユーザーID</label>
<input
id="userId"
aria-label="ユーザーID"
value={userId}
onChange={(e) => setUserId(e.target.value)}
onBlur={() => setTouched(true)}
/>
{touched && userId === '' && (
<p role="alert" data-testid="error">
ユーザーIDは必須です
</p>
)}
<button type="button">登録</button>
</>
);
}
test('userEvent はユーザー操作として書ける', async () => {
const user = userEvent.setup();
render(<RegisterForm />);
const input = screen.getByLabelText('ユーザーID');
// クリックしてフォーカス → 何も入力せず tab で blur(= 実操作っぽい)
await user.click(input);
await user.tab();
expect(screen.getByTestId('error')).toHaveTextContent('ユーザーIDは必須です');
});
userEventの方がシンプルにかけてますね。
実際の動作の違いは?
user-even(userEvent)の方が実際にユーザーが操作する感じに違くフォーカス、入力、マウスアップといった連続したイベントを検知しやすいです
import React, { useState } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function FocusWatcher() {
const [focused, setFocused] = useState(false);
return (
<>
<input
aria-label="search"
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
/>
<p data-testid="status">{focused ? 'focused' : 'not-focused'}</p>
</>
);
}
test('fireEvent.click は focus まで再現しない場合がある', () => {
render(<FocusWatcher />);
const input = screen.getByLabelText('search');
fireEvent.click(input); // click だけ発火しがち
// focus が入らず、状態が変わらないケースがある
expect(screen.getByTestId('status')).toHaveTextContent('not-focused');
});
test('userEvent.click は実操作に近く focus まで入る', async () => {
const user = userEvent.setup();
render(<FocusWatcher />);
const input = screen.getByLabelText('search');
await user.click(input); // pointer/mouse 系 + focus まで起きやすい
expect(screen.getByTestId('status')).toHaveTextContent('focused');
});
感想
よっぽどの理由がないのであれば、基本はuser-event(userEvent)を使った方がいいですね。
参考