1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

テストを書いていて、fireEventuserEventがあり似たような使い方をするなと思って、違いはなんだろうと思い、調べたのでまとめます。

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)を使った方がいいですね。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?