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
を使う# npmnpm i-D @testing-library/react# yarnyarnadd-D @testing-library/react
@testing-library/react
の基本はrender
関数です。
/** * @jest-environment jsdom */importReactfrom'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
によるカスタムマッチャーも便利です。
# npmnpm i -D @testing-library/jest-dom @types/testing-library__jest-dom# yarnyarn add -D @testing-library/jest-dom @types/testing-library__jest-dom
詳しくはhttps://github.com/testing-library/jest-dom を参照してください。
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(<ButtononClick={()=>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/link
だのnext/router
だのを使っている場合、どうしてもこれらをモックする必要があります。
jest.mock('next/link',()=>{constLink=({ href, children,}:{ href:string children:string}):JSX.Element=>{return<ahref={href}>{children}</a>}returnLink})
ここではいったん<a>
にしていますがべつに<hoge>
でもなんでもいいです。どうせこれをもとに実際に HTML として動かすわけではないからです。
テスト対象で<Link>
がちゃんと<a>
に展開されていることさえ確認できれば OK です。
expect(container.innerHTML).toMatch('<a href="http://example.com">hoge</a>')
Link
の子要素が単純にならないケースなら'<a href="http://example.com">'
とだけマッチすればいいでしょう。
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。