Open11

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

yuta4j1yuta4j1

userEvent.click() と fireEvent.click() どちらを使う?

user-event が推奨される。

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

https://testing-library.com/docs/user-event/intro/#differences-from-fireevent

yuta4j1yuta4j1

非同期処理的な操作におけるテストの注意点

ユーザー操作によるイベント発生後の画面更新をテストしたい場合、waitFor で画面更新を待機することができる。
https://testing-library.com/docs/dom-testing-library/api-async#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 系のクエリを実行することで、画面更新を待機することができる。
https://testing-library.com/docs/guide-disappearance#waiting-for-appearance
例えば以下の例では、 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は必須入力です。')
  })
yuta4j1yuta4j1

要素へのフォーカスのテストは、以下のように、document.activeElementでフォーカスの当たっている要素を取得することができる。

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

複数テストケースにおいて、コンポーネントのレンダリングの記述を毎回書く必要があるので、以下のように beforeAll などで全テスト共通の処理として切り出したくなる。

test('xxxx', () => {
  beforeAll(() => {
    render(<UserNameForm />)
  })
})

しかし、この書き方はアンチパターンとして推奨されていない。
https://kentcdodds.com/blog/avoid-nesting-when-youre-testing

yuta4j1yuta4j1

APIへのデータフェッチを含むコンポーネントのテスト

サーバへのリクエストをモック化したい場合は、 jest.spyOn() を利用する
https://jestjs.io/ja/docs/jest-object#jestspyonobject-methodname

spyOnは、引数に与えられたオブジェクトのメソッドをモック関数に入れ替えることができる。
こんな感じに↓

video.js
export function play() {
  return true;
}
video.test.js
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()
  })
yuta4j1yuta4j1

書くべきだったテスト 書かなくてよかったテスト
https://speakerdeck.com/takefumiyoshii/hurontoentonoshu-kuhekitatutatesuto-shu-kanakuteyokatutatesuto

ルーティングのテストは書くべき

searchParamsの参照、値設定
例えば、?foo=bar から値を取得し、const foo = "bar" (string) が正しい挙動とする。
?foo=bar&foo=hoge の場合、const foo = ["bar", "hoge"] (string[]) になる。
これをas string で握りつぶしてない?など。

要件が複雑な関数のテスト

関数にはテストをしっかり書こう

コンポーネントの肥大化防止

ロジックに単体テストを書く
コンポーネントの肥大化防げた

書くべきでないテスト

過剰なVRT

全コンポーネントにVRT。
CI遅くなる。
VRTが本当に必要な、不具合の起こりやすいコンポーネントにフォーカス。

yuta4j1yuta4j1

react-testing-library と playwright、どう使い分けるべき?

Testing Trophy という考えがある。
https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications

ここでは、

  • 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に止める。
まずはテストのパフォーマンスは気にしたい。
(↓参考)
https://tech.readyfor.jp/entry/2021/12/04/000917

https://kentcdodds.com/blog/common-testing-mistakes#mistake-number-3-repeattesting

yuta4j1yuta4j1

テストを書く目的を明確にする

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