Reactで書いた画面をテストする

Reactで書いた画面をテストするには

Reactで書いた画面をテストしたいとなった時に、思いつくのは以下の4つの方法があるかと思います。

  • 目視で動作を確認?
  • puppeteer(E2E)?
  • enzyme?
  • React Testing Library?

次に上記の手法の特徴を考えます。

目視で動作を確認

画面が単純なうちはいいですが、画面が増えたり複雑になっていくと動きを網羅するのに時間がかかります。
また、リファクタ前後で挙動が同じであることを確認しないとデグレが起きてしまうことが考えられます。
動きの仕様を覚えておくことも大変です。

puppeteer(E2E)

起動が遅かったり、実行時間が長かったりします。
page.waitFor(2000)とか書かなければいけないのも面倒です。
正しいテストでも落ちたり、落ちなかったりして不安定になりやすいです。

enzyme

TDD like にテストが書けるため、Unit テストが書きやすいです。
コンポーネントの見た目の挙動よりも、コードの実装に対するテストが書きやすいです。
ユーザに見えている見た目部分以外をテストしがちになります。

React Testing Library

BDD(振舞駆動開発) likeにテストが書けるため、integration テストが多くなります。
コードの実装よりもユーザに対する見た目や、挙動がテストできます。
full dom レンダリングしか使えないので API 通信や redux などは mock しておく必要があります。

どれを使うべきか?

ではどれを使うべきなのでしょうか?
公式はReact Testing Libraryを勧めています。

We recommend using React Testing Library
which is designed to enable and encourage
writing tests that use your components as the end users do.

EnzymeもReactに関するテスト方法を調べるとよく出てくる方法なのですが、React Testing LibraryはEnzymeに代わるものとしてリリースされたものです。

VercelのCEOのツイートで以下のようなツイートがありました。

Write tests. Not too many. Mostly integration.

テストを書こう。多く書かなくていい。ほとんどインテグレーションテストでいい。

上記ツイートから画面のテストに関しては、単体テストを書くよりも画面全体の振る舞いをテストした方が良い、つまりTDDよりもBDDを書いた方が良いのではと考え、React Testing Libraryを使うことにしました。

React Testing Libraryのセットアップ

create-react-app

create-react-app を使用した場合は、デフォルトで使用可能です。

自分でセットアップする場合

以下を実行し、必要なライブラリをダウンロードします。

yarn add -D jest jest-dom @testing-library/react
  @testing-library/jest-dom @testing-library/dom babel-jest

setupTests.jsに以下を書き、setUp時にimportするようにします。これをしないと toBeInTheDocumentなどの、jest-domアサーション関数が使えません。

import "@testing-library/jest-dom/extend-expect";

アサーション関数

アサーション関数には以下が使えます。

  • jest のアサーション関数
    • 表示時にこの関数が呼ばれているかなどを主に確認
    • 要素の存在確認にも使うこともある
  • jest-dom のアサーション関数
    • 要素の存在確認は主にこちらのアサーション関数を使う

jestのアサーション関数

  • toHaveLength
    • いくつ存在するかを確認
  • toHaveBeenCalled(With)
    • mock した関数が呼ばれているかを確認
  • ...その他

jest-domのアサーション関数

  • toBeInTheDocument
    • 要素がドキュメントに存在することを確認
  • toBeDisabled
    • 要素が disabled になっていることを確認
  • toBeEnabled
    • 要素が enabled になっていることを確認
  • toHaveFormValues
    • Form の key,value の対応を確認できる
  • ...その他

React Testing Libraryの機能

React TestingLibraryを使用するには、まず以下のようにimportする必要があります。

import { render, fireEvent, waitFor, screen, within } from '@testing-library/react'

主に5つの機能があり、使いたいものをimportすることになります。
それぞれの役割は簡単にいうと以下になります。

  • render
    • DOMにReactのエレメントをレンダーします
    • テストしたいcomponentに対して最初にrenderをする必要があります
  • screen
    • レンダーされた内容はscreenが持っています
    • この後説明するクエリ関数を持っています
  • fireEvent
    • イベントを発生させてボタンのクリックなどのユーザアクションをシミュレートします
  • waitFor
    • 非同期処理が完了するのを待ちます
  • within
    • DOM要素を受け取り、それをscreenが持っているような、クエリ関数にバインドします
    • div単位で確認したい場合などに便利です

render時に取得できる機能

const {debug, rerender} = render(<App />)
  • debug
    • その時点での DOM の構造を確認できます(デバッグ用)
  • rerender
    • 再レンダリングすることができます

要素の取得

テストをするにはDOM要素を取得する必要があります。
screenはDOM要素を取得するために大きく分けて以下3つのクエリ関数を持っています。

  • getByXXX
  • queryByXXX
  • findByXXX

getByXXX

  • getByText
    • expect(screen.getByText('0')).toBeInTheDocument()
    • 上記の書き方で画面に0 が存在することを確認します
  • getByRole
    • aria-label属性で要素を取得します
    • buttonやtableなど暗黙的なroleもあります
    • screen.getByRole(''); のように利用できないroleを指定すると、利用できるroleをサジェストしてくれます
  • getByLabelText
    • <label for="search" />
  • getByPlaceholderText
    • <input placeholder="Search" />
  • getByAltText
    • <img alt="profile" />
  • getByDisplayValue
    • <input value="JavaScript" />

queryByXXX

  • queryByText
  • queryByRole
  • queryByLabelText
  • queryByPlaceholderText
  • queryByAltText
  • queryByDisplayValue

findByXXX

  • findByText
  • findByRole
  • findByLabelText
  • findByPlaceholderText
  • findByAltText
  • findByDisplayValue

getBy・queryBy・findByの違い

  • getByXXX
    • 要素が取得できなかった場合はエラーになります
  • queryByXXX
    • 要素がない場合でもエラーにならないため、要素がないことの確認に使えます
  • findByXXX
    • 非同期処理で、今は画面に存在しないが、最終的に存在する要素についての確認に使います

複数要素の取得

画面に0 が複数ある場合、XXXByText使うとエラーになります。複数要素を確認したい場合はXXXAllByTextを使います。
例えば0 が 2 つあることを確認したい場合は、expect(screen.getAllByText('0')).toHaveLength(2) とすることで確認できます。

data-testid属性

React Testing Libraryではdata-testidという属性が使えます。data-testidを持つ要素はXXXByTestidで取得することができます。

<button data-testid='plus-button'>+</button>

withinでDOMを分割する

たとえばファイルアップロードフォームと検索フォームがある画面があるとします。両方のフォームにリセットボタンがある場合は以下でも確認ができます。

expect(screen.getAllByText('リセット')).toHaveLength(2)

ただ、ファイルアップロードの form に一つリセットを持っていて、検索フォームにもリセットを一つ持っていることを確認できた方がより確実です。
その場合はwithinでDOMを分割します。ファイルアップロードフォームにdata-testid='upload-form' を持たせて以下のようにすると、ファイルアップロードフォームだけを持った DOM を取得できます。

const uploadForm = within(screen.getByTestId('upload-form'))
expect(uploadForm.getByText('リセット')).toBeInTheDocument()

こうすることでファイルアップロードフォームにはリセットという要素が1つしかないのでgetByText を使用することができます。

まとめ

今回はReactで書いた画面のテスト方法として、React Testing Libraryの使い方を説明しました。
今回説明した内容だけで簡単にテストを書くことが可能です。ただしAPI通信を行なっている画面の場合は、その部分をmockしないといけないため、もう少し工夫が必要です。それはまた別の記事で説明したいと思います。

参考