Webフロントエンドテストの知見を集める

React Testing Library の頻出テストケース集

userEvent.click() と fireEvent.click() どちらを使う?
user-event が推奨される。
user-eventは、ブラウザ上でのイベントの発生を忠実に再現する。
一方でfireEventは React Testing Library の組み込みオブジェクトで、一つのイベントを擬似的にディスパッチする。しかし、ユーザーの一アクションにより引き起こされるイベントは、一つではないという意味で、よりブラウザの動きを忠実に再現してくれる、user-eventを使うべき、という話。

非同期処理的な操作におけるテストの注意点
ユーザー操作によるイベント発生後の画面更新をテストしたい場合、waitFor
で画面更新を待機することができる。
例えば文字が入力されていない場合にエラーメッセージを表示するフォーム画面において、「必須エラーメッセージが表示されていること」を確認するテストは以下のように書くことができる。
test('FirstNameが空', async () => {
render(<UserNameForm />)
const submitButton = screen.getByText('確定する')
// 「確定」ボタンをクリック
await userEvent.click(submitButton)
waitFor(() => {
const alertElms = screen.getAllByRole('alert')
expect(alertElms && alertElms[0]).toHaveTextContent('firstNameは必須入力です。')
})
})
一方、都度 waitFor を書かずとも、findBy
系のクエリを実行することで、画面更新を待機することができる。
例えば以下の例では、 findAllByRole
を実行すると、waitForと同様に更新後の画面のテストを実施できる。
test('FirstNameが空', async () => {
render(<UserNameForm />)
const submitButton = screen.getByText('確定する')
// 「確定」ボタンをクリック
userEvent.click(submitButton)
const alertElms = await screen.findAllByRole('alert')
expect(alertElms[0]).toHaveTextContent('firstNameは必須入力です。')
})

要素へのフォーカスのテストは、以下のように、document.activeElement
でフォーカスの当たっている要素を取得することができる。
test('要素が複数エラーになった際、先頭の要素にフォーカスが当たっていること', async () => {
render(<UserNameForm />)
// 何も入力せず、「確定」ボタンをクリック
const submitButton = screen.getByText('確定する')
await userEvent.click(submitButton)
const activeElm = document.activeElement
expect(activeElm).toBe(screen.getByLabelText('FirstName'))
})

複数テストケースにおいて、コンポーネントのレンダリングの記述を毎回書く必要があるので、以下のように beforeAll
などで全テスト共通の処理として切り出したくなる。
test('xxxx', () => {
beforeAll(() => {
render(<UserNameForm />)
})
})
しかし、この書き方はアンチパターンとして推奨されていない。

APIへのデータフェッチを含むコンポーネントのテスト
サーバへのリクエストをモック化したい場合は、 jest.spyOn()
を利用する
spyOnは、引数に与えられたオブジェクトのメソッドをモック関数に入れ替えることができる。
こんな感じに↓
export function play() {
return true;
}
import video from './video';
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
test('plays video', () => {
const spy = jest.spyOn(video, 'play');
const isPlaying = video.play();
expect(spy).toHaveBeenCalled();
expect(isPlaying).toBe(true);
});
サンプル
一例として、旧来の useEffect
を使用したデータフェッチを行うコンポーネントがあったとして
import React, { useEffect, useState } from 'react'
import { getRequest } from '../../api'
export const SampleRequest = () => {
const [message, setMessage] = useState('')
useEffect(() => {
;(async () => {
const data = await getRequest<{ result: string }>('hello')
setMessage(data.result)
})()
}, [])
return (
<div>
<h3>SampleRequest</h3>
<p>Data: {message}</p>
</div>
)
}
リクエストのモック処理をこんな感じで定義する。
import * as Fetcher from '.'
const mockData = { result: 'test ok!' }
export function mockGetHello() {
return jest.spyOn(Fetcher, 'getRequest').mockResolvedValueOnce(mockData)
}
テストはこう書く
test('success fetch hello', async () => {
mockGetHello()
render(<SampleRequest />)
expect(await screen.findByText(/test ok!/i)).toBeInTheDocument()
})

書くべきだったテスト 書かなくてよかったテスト
ルーティングのテストは書くべき
searchParamsの参照、値設定
例えば、?foo=bar
から値を取得し、const foo = "bar" (string)
が正しい挙動とする。
?foo=bar&foo=hoge
の場合、const foo = ["bar", "hoge"] (string[])
になる。
これをas string
で握りつぶしてない?など。
要件が複雑な関数のテスト
関数にはテストをしっかり書こう
コンポーネントの肥大化防止
ロジックに単体テストを書く
コンポーネントの肥大化防げた
書くべきでないテスト
過剰なVRT
全コンポーネントにVRT。
CI遅くなる。
VRTが本当に必要な、不具合の起こりやすいコンポーネントにフォーカス。

HooksのテストにはrenderHooksを使う

react-testing-library と playwright、どう使い分けるべき?
Testing Trophy という考えがある。
ここでは、
- Static
- Unit Test
- Integration Test
- E2E Test
と言う分類で、どのテストを多く書くべきか、と言う話がされている。
E2Eテストは品質を強く担保するが、どうしても実行パフォーマンスが落ちる。
一方 Integration Test や Unit Test は、品質を強く担保するわけではないがパフォーマンスに優れている。
react-testing-library は Integration Test / Unit Test を書くことができる。
playwrightでは、主にE2Eテストを書くことができるが、Integration Test のようなこともできる。
Integration Testは、なるべくreact-testing-libraryで書くべし。
そして playwright は、E2Eに止める。
まずはテストのパフォーマンスは気にしたい。
(↓参考)

テストを書く目的を明確にする
テストはあくまで品質担保のためであり、「テストを書くこと」や「カバレッジ100%」が目的になってはならない。
効果的なテストを書くことが、何よりも重要。

コンポーネントの品質担保の手段として、Storybookを使うのは良さそう。
VRTもできるし。