🧪

React Testing LibraryのfireEvent vs. user-event

2023/07/31に公開

Reactの統合テストを書くときに、fireEventとuser-eventのどちらを使うか迷うことがあります。調べるとuser-event使用した方がいいとの声が多いです。
そもそも同じような結果が得られるのに、user-eventを使う必要があるのかという疑問がありました。

今回はこの疑問に対して、筆者が調べて得た結果を共有していきます。

fireEventとuser-event

fireEventとuser-eventは、ReactのテストライブラリであるReact Testing Libraryに含まれている関数です。

fireEventはDOMイベントを発火させるための関数です。

import { fireEvent } from '@testing-library/react'
// ボタンをクリックする
fireEvent.click(screen.getByRole('button'))

user-eventは、ユーザーが実際に行う操作をシミュレートするための関数です。

import userEvent from '@testing-library/user-event'
// ボタンをクリックする
userEvent.click(screen.getByRole('button'))

公式の見解

fireEventの使用例がいくつかありますが、大半の場合は@testing-library/user-eventを使用するべきです、と書かれていますね。

Most projects have a few use cases for fireEvent, but the majority of the time you should probably use @testing-library/user-event.
https://testing-library.com/docs/dom-testing-library/api-events/

両者の差を検証してみる

両者は似たようなことができる関数ですが、どのような差があるのでしょうか。

fireEventよりuser-eventの方が簡潔に書ける

よく言われているのが、fireEventよりuser-eventの方が簡潔に書けるということです。

例えば、インプットがあり文字を入力するというテストがあった場合、以下のようになります。
比較すると、user-eventの方が簡潔に書けています。わざわざDOMイベントを発火させる必要がないので、簡潔に書けるのは当然です。

import { fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

// fireEvent
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test' } })
// user-event
userEvent.type(screen.getByRole('textbox'), 'test')

しかし、これだけではuser-eventを使う決定打に欠けます。そこで、次に紹介する内容が重要になってきます。

ユーザーが実際に行う操作をシミュレートする

fireEventとuser-eventのどちらを使っても、DOMイベントを発火させることができます。しかし、user-eventを使った方が、よりユーザーが実際に行う操作に近いテストが書けます。

例えば、名前を入力して内容を表示させるという簡単なテストを実施します。SampleComponentisDisabled = trueを渡すと、入力できないようにしています。
ここで望む挙動は、フォーム非活性の場合は入力できない、かつ何も表示されないことです。

まずはfireEventを使用したテストを書きます。

type Props = {
  isDisabled?: boolean;
}

const SampleComponent: FC<Props> = ({
  isDisabled = false,
}) => {
  const [name, setName] = useState("");

  return (
    <div>
      <input
        type="text"
        placeholder="Enter your name"
        value={name}
        onChange={(e) => setName(e.target.value)}
        disabled={isDisabled}
      />
      <p>{name}</p>
    </div>
  );
}

test("フォーム非活性の場合、入力できず値も表示されない", () => {
  const { getByRole, queryByText } = render(<SampleComponent isDisabled />);

  const input = getByRole("textbox");
  // テキストボックスは非活性である
  expect(input).toBeDisabled();
  // 非活性であるが、入力値を変更できてしまう
  fireEvent.change(input, { target: { value: "John" } });
  // 本来は表示されないが、表示されるのでテストが通ってしまう
  expect(queryByText("John")).toBeInTheDocument();
})

fireEventを使用して入力値を変更した場合、本来は表示されないはずの入力値が表示されています。これは、fireEventはDOMイベントを発火させるだけで、ユーザーが実際に行う操作をシミュレートできないためです。

一方、user-eventを使用してみましょう。この問題を解決するために、実際にユーザーが行う操作をシミュレートできます。

...(省略)

test("フォーム非活性の場合、入力できず値も表示されない", () => {
  const user = userEvent.setup();
  const { getByRole, queryByText } = render(<SampleComponent isDisabled />);

  const input = getByRole("textbox");
  // テキストボックスは非活性である
  expect(input).toBeDisabled();
  // ユーザーが入力しようと試みるが、非活性のため入力できない
  user.type(input, "John");
  // 表示されないので、正しい
  expect(queryByText("John")).toBeNull()
})

まとめ

私自身、fireEventとuser-eventのどちらを使っても同じような結果が得られると思っていました。
しかし、user-eventを使うことで、よりユーザーが実際に行う操作をシミュレートできることが分かりました。

両者の差が分かった上で、筆者はよりユーザー操作に忠実なテストを書くために、user-eventを使っていきたいと考えています。

Discussion