Reactのユニットテスト2021
React でユニットテストをするときのベストプラクティスはいつも悩むのですが、とりあえず 2021 年 2 月時点では、こうかなーというのをまとめてみます。
まずテストランナーは jest で確定です。ここで悩む要素はまずありません。
では、React のテストをどうやるか?です。
- 公式の
react-dom/test-utils
を使う - 公式の
react-test-renderer
を使う -
@testing-library/react
を使う
選択肢としてはこの三種類が有名なところでしょう。
公式という響きはとても魅力的ですが、実は公式ドキュメントから「ボイラープレートを減らすため、エンドユーザが使うのと同じ形でコンポーネントを使ってテストが記述できるように設計されている、React Testing Library の利用をお勧めします。」という形で、@testing-library/react
が推奨されてもいます。
react-test-renderer
を使った参考資料は割とありますが、これはすでに @testing-lirary/react
に置き換えられてます(renamed?)。
同様に React Hooks のテストも、公式が説明している手段では、めちゃくちゃ面倒です。@testing-library/react-hooks
がとても簡単です。
そういった理由により、現時点で採用するなら @testing-library/react
一択かなーと思っております。とりあえず @testing-library
ファミリー使っておけば間違いはないでしょう。
@testing-library/react
を使う
# npm
npm i -D @testing-library/react
# yarn
yarn add -D @testing-library/react
@testing-library/react
の基本は render
関数です。
/**
* @jest-environment jsdom
*/
import React from 'react'
import { render } from '@testing-library/react'
import { Hoge } from '.'
test('Hoge', () => {
const renderResult = render(<Hoge />)
// expect...
})
render
の戻り値を使って expect
を記述します。戻り値は RenderResult
型です。詳しくは https://testing-library.com/docs/react-testing-library/api/#render-result を参照してください。
@testing-library/jest-dom
拡張マッチャー
@testing-library/jest-dom
によるカスタムマッチャーも便利です。
# npm
npm i -D @testing-library/jest-dom @types/testing-library__jest-dom
# yarn
yarn add -D @testing-library/jest-dom @types/testing-library__jest-dom
詳しくは https://github.com/testing-library/jest-dom を参照してください。
snapshot test
test('snapshot testing', () => {
const { asFragment } = render(<Hoge />)
expect(asFragment()).toMatchSnapshot()
})
DOM 構造のスナップショットをとって、変化したらエラーになるタイプのテストです。変化が妥当だと判断できるなら jest -u
でスナップショットのアップデートをします。
文字列マッチング
test('matching text', () => {
const { container } = render(<Hoge />)
expect(container.innerHTML).toMatch('hoge')
})
吐き出される HTML に hoge という文字列が含まれていれば OK というテストです。toMatch
マッチャーは、テキストもしくは正規表現が利用できます。HTML に文字列マッチングをするのでいささか乱暴です。
@testing-library/jest-dom
拡張マッチャーを使っている場合は、.toHaveTextContent
でそのものずばりテキストコンテンツのテストができます。
expect(container).toHaveTextContent('hoge')
クラス名
test('matching text', () => {
const { container } = render(<Hoge />)
expect(container.getElementsByClassName('fuga').length).toEqual(1)
})
クラス名 fuga
を持つエレメントが 1 つあれば OK というテストです。1 つとは限らない場合は .toBeGreaterThan(0)
のように別のマッチャーを使いましょう。
jest-dom
拡張マッチャーを使っていれば、toHaveClass
マッチャーを使うという方法もあります。
test('Hoge', () => {
const { getByText } = render(<Hoge />)
expect(getByText('hoge')).toHaveClass('fuga')
})
getByText('hoge')
により hoge
というテキストを持つエレメントを取得しそのクラス名に fuga
が含まれているかテストしています。より精密なテストを書く場合には考慮に入れてもいいかもしれません。
イベントハンドラのテスト
たとえば Button
というコンポーネントを用意して onClick
ハンドラをテストするとします。
import { render, fireEvent } from '@testing-library/react'
test('onClick', () => {
const handleClick = jest.fn()
const { getByText } = render(
<Button onClick={() => handleClick()}>hoge</Button>,
)
fireEvent.click(getByText('hoge'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
まず jest.fn()
でモック関数を生成し、onClick の引数に渡します。
次に @testing-library
の fireEvent
オブジェクトを使ってイベントを発火します。クリックイベントを発生させる場合は fireEvent.click(getByText('hoge'))
のようにします。
そのあと expect(handleClick).toHaveBeenCalledTimes(1)
のようにモック関数が 1 回呼び出されたことを確認します。
<input type="text" onChange={...} />
をテストするような場合は、第 2 引数にイベントを指定する必要があるでしょう。
文字入力コンポーネントを作るときは、扱いづらいイベントを直接触るよりも (text: string) => void
のような直接文字列を受け取れるハンドラを書くことも多いでしょう。
fireEvent.input(element, { target: { value: 'hoge' } })
expect(handleChange0).toHaveBeenCalledTimes(1)
expect(handleChange0.mock.calls[0][0]).toEqual('hoge')
この場合、モック関数の呼び出し回数が 1 回で、結果としてハンドラが hoge
というテキストを受け取ったというテストが可能です。
Next.js のユニットテスト
最近は Next.js が当たり前になりました。
Next.js の next/link
だの next/router
だのを使っている場合、どうしてもこれらをモックする必要があります。
jest.mock('next/link', () => {
const Link = ({
href,
children,
}: {
href: string
children: string
}): JSX.Element => {
return <a href={href}>{children}</a>
}
return Link
})
ここではいったん <a>
にしていますがべつに <hoge>
でもなんでもいいです。どうせこれをもとに実際に HTML として動かすわけではないからです。
テスト対象で <Link>
がちゃんと <a>
に展開されていることさえ確認できれば OK です。
expect(container.innerHTML).toMatch('<a href="http://example.com">hoge</a>')
Link
の子要素が単純にならないケースなら '<a href="http://example.com">'
とだけマッチすればいいでしょう。
Discussion