Testing Libraryのテストは fireEvent より user-event を使おう

はじめましての人ははじめまして。そうでないひとはお久しぶりです。猫ロキP(@deflis/id:deflis55)です。

みなさんはReactでテスト書いてますか?私は最近書き始めました。

タイトル通りなんですが Testing Library のテストは、Testing Libraryに標準で入っている fireEvent よりも、外部ライブラリの @testing-library/user-event を使おうというお話です。

user-event はシンプルに書ける上に、メリットがたくさんあるのでおすすめのライブラリです。

なぜ user-event を使うべきなのか

なぜこれを使おうというになるか話かというと、 fireEvent は単純にDOMイベントを発生させるのに対し、 user-event は全ての入力に対するイベントを発生させるからです。

でも、それ以上に簡素に書けることが便利です。 本エントリでは、「簡素に書けること」「全ての入力に対するイベントが発生すること」の2つの側面から、このライブラリの記述例を作ってみました。

特にtextboxへの入力が便利

以下のような入力フォームにテストしたいとします。

const Input: React.FC<{ defaultValue?: string }> = ({ defaultValue = '' }) => {

const [value, setValue] = React.useState(defaultValue);

return <input type="text" value={value} onChange={e => setValue(e.target.value)} />;

};

普通にfireEventを使うと、以下のようなコードになると思います。

test("単純な入力テスト", () => {
    render(<Input />);
    
    act(() => {
        fireEvent.change(screen.getByRole("textbox"), {
            target: {
                value: "hello!"
            }
        });
    })
    expect(screen.getByRole("textbox")).toHaveValue("hello!");
]);

actで包無必要があってボイラープレートが多くて面倒だと思いませんか?また、イベントの値に target.value しか渡してないので、他のイベントの値を参照してると壊れてしまうと思います。

これがこう書けるようになります。

test('userEventの場合', async () => {
    const user = userEvent.setup();
    render(<Input />);
    
    await user.type(screen.getByRole('textbox'), 'hello!');
    expect(screen.getByRole('textbox')).toHaveValue('hello!');
});

どうでしょう?だいぶスッキリ書けるようになりましたね。 これだけでも導入する価値があると思いませんか?

付随イベントが発生することのメリット

メリットはこれだけではありません。

最初の例の場合では単純なコンポーネントのためテストが成功するのですが、この Input が文字入力にあわせてなにかしていた場合、問題が発生します。なぜなら、onChangeイベント以外のイベントは発生しないからです。

元の Input をこう書き換えてみましょう。(こんなコンポーネントは普通は存在しませんが、動作確認のために書きました。)

const Input: React.FC<{ defaultValue?: string }> = ({ defaultValue = '' }) => {
    const [value, setValue] = React.useState(defaultValue);]
    const [keyDown, setKeyDown] = React.useState(false);
    
    return (
        <div>
            <input type="text" value={value} onChange={e => setValue(e.target.value)} onKeyDown={() => setKeyDown(true)} />
            {keyDown && <p>onKeyDownが発生しました</p>}
        </div>
    );
};

どう変わったかというと、 onKeyDown を監視するようにしました。 普通にキーボードで入力したら onKeyDown が発生するはずです。

なので、テストを以下のように書き換えてみました。

test('onKeyDownのテスト', () => {
    render(<Input />);

    act(() => {
        fireEvent.change(screen.getByRole('textbox'), {
            target: {
                value: 'hello!',
            },
        });
    });
    expect(screen.getByRole('textbox')).toHaveValue('hello!');
    expect(screen.getByText('onKeyDownが発生しました')).toBeInTheDocument();
});

ですが、このテストはどうでしょうか? もちろん、 onKeyDown は発生しないので失敗します。 例えば、検索のサジェストのテストとかで困りそうですよね?

一方、 user-event で書くとこうなります。

test('userEventのonKeyDownのテスト', async () => {
    const user = userEvent.setup();
    render(<Input />);
    
    await user.type(screen.getByRole('textbox'), 'hello!');
    expect(screen.getByRole('textbox')).toHaveValue('hello!');
    expect(screen.getByText('onKeyDownが発生しました')).toBeInTheDocument();
});

当たり前なんですが、こちらはちゃんと成功します。 なぜなら、 user-event は全てのキーボード入力をシミュレーションしてくれるからです。 これなら、検索のサジェストのテストなどもしっかり書くことが出来ますね。

まとめ

user-eventは、とにかく記述量が減って便利で、Reactでテスト書いてる人は絶対に導入したほうがいいライブラリでした。

注意点としては、テストが遅くなる副作用もなくはないです。 例えば、100文字を超えるような文字を入力しようとすると100文字分のキーストロークをシミュレーションしてしまうのでかなりテストに時間がかかるようになります。 その場合は fireEvent で書くと余計なイベントが発生しないで済むので、使い分けすれば良いと思います。

というわけで、万能ではないですが、簡素に書けて、考えることも減る @testing-library/user-event はかなりおすすめです。

特に脈略のない今回のおすすめラノベコーナー

今月6巻が出てホクホクしてるギルますがおすすめです。

スレイヤーズを彷彿とさせる、男女バディ(?)の絶妙な距離感ラブコメファンタジーです。ここ最近見なかった感じなので、そういう作品が読みたい、読んでみたい人におすすめです!

https://bookwalker.jp/de942988a9-558e-4bbd-ad18-1f6297172acc/?acode=eh0lFyk1

comic-walker.com