69
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Common mistakes with React Testing Library の和訳

Posted at

本記事は Kent C. Dodds氏による Common mistakes with React Testing Library の和訳です。

React Testing Library のよくある間違い

こんにちは👋 私がReact Testing Libraryを作ったのは、当時のテストの状況に満足していなかったからです。
それが DOM Testing Library に発展し、今ではすべての人気のある JavaScript フレームワークと DOM を対象とした(そうでないものも含めて)テストツールのための実装(ラッパー)があります。

時間の経過とともに、私たちは API にいくつかの小さな変更を加え、最適ではないパターンを発見してきました。
私たちが提供するユーティリティを使用する「より良い方法」を文書化しようと努力しているにもかかわらず、私はいまだにこのような最適ではないパターンで書かれたブログ記事やテストを目にすることがあります。
そのいくつかを紹介し、なぜ良くないのか、そしてこれらの落とし穴を避けるためにどのようにテストを改善できるのかを説明したいと思います。

注: それぞれの重要度に応じてラベルを付けています。

  • 低: これはほとんど私の意見ですので、無視していただいても構いませんし、おそらく問題はないでしょう。
  • 中: バグが発生したり、自信を失ったり、必要のない仕事をしてしまったりするかもしれません。
  • 高: このアドバイスは絶対に聞いてください!自信を失っている可能性が高いか、問題のあるテストを抱えています。

Testing Library の ESLint プラグインを使っていない

重要度: 中

よくある間違いを避けたいのであれば、公式の ESLint プラグインが大いに役立ちます。

注: create-react-app を使用している場合は、eslint-plugin-testing-library がすでに依存関係として含まれています。

アドバイス

Testing Library の ESLint プラグインをインストールして利用する。

render からの戻り値の変数名として wrapper を使っている

重要度: 低

// ❌
const wrapper = render(<Example prop="1" />)
wrapper.rerender(<Example prop="2" />)

// ✅
const {rerender} = render(<Example prop="1" />)
rerender(<Example prop="2" />)

wrapper という名前は enzyme の名残です。
render からの戻り値は何も「ラップ」していません。
実際にはあまり使うことがないユーティリティが戻ります。

アドバイス

render から必要なものを分割代入するか、 view という名前をつける。

cleanup を使っている

重要度: 中

// ❌
import {render, screen, cleanup} from '@testing-library/react'

afterEach(cleanup)

// ✅
import {render, screen} from '@testing-library/react'

長い時を経て、今では cleanup は自動的に行われる(ほとんどの主要なテストフレームワークでサポートされています)ので、もう気にかける必要はありません。
詳しく学ぶにはこちら

アドバイス

cleanup は使わない。

screen を使っていない

重要度: 中

// ❌
const {getByRole} = render(<Example />)
const errorMessageNode = getByRole('alert')

// ✅
render(<Example />)
const errorMessageNode = screen.getByRole('alert')

screenDOM Testing Library v6.11.0 で追加されました。(@testing-library/react@>=9 で利用可能)
render と同じ import 元から取得できます。

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

screen を使用することの利点は、必要なクエリを追加もしくは削除する際に、render の分割代入を最新の状態に保つ必要がなくなることです。
screen と入力するだけで、あとはエディタのマジックオートコンプリートが処理してくれます。
唯一の例外は、 containerbaseElement を設定している場合ですが、これはおそらく避けるべきでしょう(正直なところ、これらのオプションの正当な使用例はもう思いつきませんし、現時点では歴史的な理由でしか存在していません)。

また debug の代わりに screen.debug を呼び出すこともできます。

アドバイス

クエリやデバッグには screen を使う。

間違ったアサーションを使っている

重要度: 高

const button = screen.getByRole('button', {name: /disabled button/i})

// ❌
expect(button.disabled).toBe(true)
// エラーメッセージ:
//  expect(received).toBe(expected) // オブジェクトの等価性
//
//  Expected: true
//  Received: false

// ✅
expect(button).toBeDisabled()
// エラーメッセージ:
//   Received element is not disabled:
//     <button />

toBeDisabled アサーションは jest-dom にあります。
表示されるエラーメッセージがより優れているため、jest-dom を利用することを強く推奨します。

アドバイス

@testing-library/jest-dom/*/* をインストールして使う。

不必要に act でラップしている

重要度: 中

// ❌
act(() => {
  render(<Example />)
})

const input = screen.getByRole('textbox', {name: /choose a fruit/i})
act(() => {
  fireEvent.keyDown(input, {key: 'ArrowDown'})
})

// ✅
render(<Example />)
const input = screen.getByRole('textbox', {name: /choose a fruit/i})
fireEvent.keyDown(input, {key: 'ArrowDown'})

このように act でラップしている人を見かけるのは、「act」警告をいつも目にしていて、なんとかそれを解消しようと必死になっているからなのですが、彼らは renderfireEvent がすでに act で包まれていることを知らないのです!
つまり、これらは何の役にも立たないということです。

ほとんどの場合、 act 警告が表示されたら、それは単に黙らせれば良いものではなく、実際にテストで予想外のことが起こっているということを教えてくれています。
これについては、私のブログ記事(とビデオ)「Fix the "not wrapped in act(...)" warning」で詳しく説明しています。

アドバイス

いつ act が必要になるのか学び、不必要に act でラップしない。

間違ったクエリを使っている

重要度: 高

// ❌
// 以下の DOM を取得したいと仮定
// <label>Username</label><input data-testid="username" />
screen.getByTestId('username')

// ✅
// label を関連付け、 type を設定することで、DOM をアクセス可能な状態に変更する
// change the DOM to be accessible by associating the label and setting the type
// <label for="username">Username</label><input id="username" type="text" />
screen.getByRole('textbox', {name: /username/i})

私たちは、 "Which query should I use?" というページで、使うべきクエリとその順位を紹介しています。
あなたの目標が、ユーザーがアプリを使ったときに動作することを確信できるテストをするという私たちの目標と一致するなら、エンドユーザーの操作方法にできるだけ近い形で DOM を見つけたいと考えるでしょう。
私たちが提供するクエリはその手助けとなるものですが、すべてのクエリが同じように作られているわけではありません。

要素のクエリのために container を使っている

「間違ったクエリを使っている」のサブセクションとして、container を直接クエリすることについて話したいと思います。

// ❌
const {container} = render(<Example />)
const button = container.querySelector('.btn-primary')
expect(button).toHaveTextContent(/click me/i)

// ✅
render(<Example />)
screen.getByRole('button', {name: /click me/i})

私たちは、ユーザーが UI を操作できることを保証したいのですが、querySelector を使ってクエリすると、その信頼性が大きく損なわれ、テストが読みにくくなり、より頻繁に壊れることになります。
これは、次のサブセクションと密接に関係しています。

テキストでクエリしていない

「間違ったクエリを使っている」のサブセクションとして、なぜテスト ID やその他のメカニズムをいたるところで使うのではなく、実際の テキストでクエリすることを推奨するのかについてお話ししたいと思います(ローカライゼーションしている場合は、デフォルトのロケールを推奨します)。

// ❌
screen.getByTestId('submit-button')

// ✅
screen.getByRole('button', {name: /submit/i})

実際のテキストでクエリを行わない場合は、翻訳が正しく適用されているかどうかを確認するために余分な作業が必要になります。
テキストでクエリすることについての最大の不満は、コンテンツライターがテストを壊してしまうことだと聞きました。
これに対する私の反論は、まず、コンテンツライターが「ユーザー名」を「電子メール」に変更した場合、それは間違いなく私が知りたい変更だということです(私の実装を変更する必要があるからです)。
また、もし何かを壊してしまったとしても、その問題の修正には時間がかかりません。
トリアージが容易で、修正も簡単です。

そのため、コストは非常に低く、利点としては、翻訳が正しく適用されているという確信が得られ、加えて テストが書きやすく、読みやすくなります。

誰もが私に同意しているわけではないということに触れておくべきなので、ツイートのスレッド も読んでみてください。

ほとんどの場合で *ByRole を使っていない

「間違ったクエリを使っている」のサブセクションとして、*ByRole についてお話したいと思います。
最近のバージョンでは、 *ByRole のクエリが大幅に改善されました(主に Sebastian Silbermann 氏の素晴らしい仕事のおかげです)。
今ではコンポーネントの出力をクエリするための一番の推奨方法となっています。
私のお気に入りの機能をいくつかご紹介します。

name オプションを使用すると、要素を"Accessible Name"でクエリすることができます。
これは、スクリーンリーダーがその要素を読み上げる際の名前です。
要素のテキストコンテンツが別の要素で分割されていても機能します。
例えば、以下のようになります。

// 以下の DOM 構造があると仮定
// <button><span>Hello</span> <span>World</span></button>

screen.getByText(/hello world/i)
// ❌ 以下のようなエラーで失敗する:
// Unable to find an element with the text: /hello world/i. This could be
// because the text is broken up by multiple elements. In this case, you can
// provide a function for your text matcher to make your matcher more flexible.

screen.getByRole('button', {name: /hello world/i})
// ✅ 動作する!

人々が *ByRole クエリを使用しない理由の一つは、要素の暗黙の役割に慣れていないからです。
MDNでの役割のリストはこちら
*ByRole クエリのもう一つの私のお気に入りの機能は、指定したロールを持つ要素が見つからなかった場合、
通常の get*find* で行うように、DOM 全体をログに出力するだけでなく、クエリで使用できるすべてのロールもログに出力することです!

// 以下の DOM 構造があると仮定
// <button><span>Hello</span> <span>World</span></button>
screen.getByRole('blah')

以下のようなエラーメッセージを出力して失敗します。

TestingLibraryElementError: Unable to find an accessible element with the role "blah"

Here are the accessible roles:

  button:

  Name "Hello World":
  <button />

  --------------------------------------------------

<body>
  <div>
    <button>
      <span>
        Hello
      </span>

      <span>
        World
      </span>
    </button>
  </div>
</body>

ボタンにボタンの役割を持たせるために、role=button を追加する必要がなかったことに注目してください。
これは暗黙の役割であり、次の項と関連します。

アドバイス

"Which Query Should I Use" ガイド の推奨事項に従う。

aria-, role, その他のアクセシビリティ属性の間違った追加

重要度: 高

// ❌
render(<button role="button">Click me</button>)

// ✅
render(<button>Click me</button>)

アクセシビリティ属性を(上記のケースのように)むやみにつけることは、不要なだけでなく、スクリーンリーダーやそのユーザーを混乱させる可能性があります。
アクセシビリティ属性は、セマンティック HTML ではユースケースを満たせない場合にのみ使用すべきです(例えば、オートコンプリートのようなアクセシブルにしたい非ネイティブ UI を構築している場合など)。
もしあなたがそのようなものを作ろうとしているのであれば、アクセシビリティに対応した既存のライブラリを使うか、WAI-ARIA のプラクティスに従うようにしてください。
素晴らしい事例が多々あります。

注: input を "role" 属性を利用してアクセシブルにする場合、 type 属性も指定しましょう!

アドバイス

不要もしくは間違ったアクセシビリティ属性を付けない。

@testing-library/user-event を使っていない

重要度: 中

// ❌
fireEvent.change(input, {target: {value: 'hello world'}})

// ✅
userEvent.type(input, 'hello world')

@testing-library/user-eventfireEvent の上に構築されたパッケージですが、よりユーザー操作に近いいくつかのメソッドを提供しています。
上の例では、 fireEvent.change は単純に input に対して一回の change イベントをトリガーします。
しかし、 type の呼び出しでは、各文字の keyDown, keyPress, keyUp イベントも同様にトリガーします。
これは、ユーザーの実際の操作に近いものです。
これには、実際には change イベントをリッスンしないライブラリを使用してもうまく動作するという利点があります。

現在、 @testing-library/user-event では、「ユーザーが特定のアクションを実行したときに発生するのと同じイベントをすべて発生させる」という約束を確実に果たすための作業を行っています。
まだまだこれからのため、 @testing-library/dom には組み込まれていません(将来的には組み込まれるかもしれません)。
しかし、私はこれに十分な自信を持っていますので、 fireEvent よりもこちらのユーティリティを使うことをお勧めします。

アドバイス

可能であれば fireEvent ではなく @testing-library/user-event を使う。

存在しないことのチェック以外の 何かquery* を使っている

重要度: 高

// ❌
expect(screen.queryByRole('alert')).toBeInTheDocument()

// ✅
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

クエリの query* が公開されている 唯一の 理由は、クエリにマッチする要素が見つからなくてもエラーを出さない
(要素が見つからなければ null を返す)関数を呼び出すことができるようにするためです。
これが便利なのは、要素がページにレンダリングされていないことを検証するとき だけ です。
このことが非常に重要な理由は、 get*find* は、要素が見つからない場合に非常に役立つエラーをスローするからです。
(ドキュメント全体が出力されるので、レンダリングされた内容を確認でき、クエリで探していたものが見つからなかった理由が分かるかもしれません。)
一方、 query*null を返すだけなので、toBeInTheDocument が言えるのは「null はドキュメント内にない」ということになり、これでは役に立ちません。

アドバイス

query* は要素が存在しないことを検証するためだけに使う。

find* でクエリできる要素の表示を待つのに waitFor を使っている

重要度: 高

// ❌
const submitButton = await waitFor(() =>
  screen.getByRole('button', {name: /submit/i}),
)

// ✅
const submitButton = await screen.findByRole('button', {name: /submit/i})

この二つのコードは基本的には同じですが(find* クエリは waitFor を使用しています)、二つ目のコードの方がシンプルで、表示されるエラーメッセージも良いものになります。

アドバイス

すぐには利用できないものをクエリしたいときには、 find* を使う。

waitFor に空のコールバックを渡している

重要度: 高

// ❌
await waitFor(() => {})
expect(window.fetch).toHaveBeenCalledWith('foo')
expect(window.fetch).toHaveBeenCalledTimes(1)

// ✅
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
expect(window.fetch).toHaveBeenCalledTimes(1)

waitFor の目的は、特定のことが起こるのを待てるようにすることです。
空のコールバックを渡すと、モックの動作のおかげで「イベントループの1回の通過」を待つだけでよく、今日時点では上手くいく かもしれません
しかし、非同期ロジックをリファクタリングすると簡単に失敗してしまうような脆弱なテストを残していくことになります。

アドバイス

waitFor 内でアサーションを待つ。

一つの waitFor コールバック内に複数のアサーションを持たせている

重要度: 低

// ❌
await waitFor(() => {
  expect(window.fetch).toHaveBeenCalledWith('foo')
  expect(window.fetch).toHaveBeenCalledTimes(1)
})

// ✅
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
expect(window.fetch).toHaveBeenCalledTimes(1)

上の例で、window.fetchが2回呼ばれたとしましょう。
そのため、waitFor の呼び出しは失敗しますが、そのテストの失敗を確認するにはタイムアウトを待たなければなりません。
ここに単一のアサーションを入れることで、UI がアサーションしたい状態に落ち着くのを待つことができ、
また、アサーションの一つが失敗してしまった場合に、より早く失敗を確認することができます。

アドバイス

コールバック内にはアサーションは一つだけ置く。

waitFor 内で副作用を起こす

重要度: 高

// ❌
await waitFor(() => {
  fireEvent.keyDown(input, {key: 'ArrowDown'})
  expect(screen.getAllByRole('listitem')).toHaveLength(3)
})

// ✅
fireEvent.keyDown(input, {key: 'ArrowDown'})
await waitFor(() => {
  expect(screen.getAllByRole('listitem')).toHaveLength(3)
})

waitFor は、アクションを実行してからアサーションが通過するまでの時間が不定であるような場合を想定しています。
このため、コールバックは不定の回数と頻度で呼び出される(またはエラーチェックが行われる)ことがあります。(インターバルでも、DOM の変更があったときにも呼び出されます)。
つまり、副作用が複数回実行される可能性があるということです!

これは、waitFor の中でスナップショットアサーションを使用できないことも意味します。
もしスナップショットアサーションを使いたいのであれば、まず特定のアサーションを待ち、その後にスナップショットを取ることができます。

アドバイス

副作用は waitFor コールバックの外に置き、コールバック内ではアサーションのみ行う。

アサーションとして get* を使っている

重要度: 低

// ❌
screen.getByRole('alert', {name: /error/i})

// ✅
expect(screen.getByRole('alert', {name: /error/i})).toBeInTheDocument()

これは実際には大したことではありませんが、言及して私の意見を述べておこうと思いました。
get* クエリが要素の検索に失敗した場合、完全な DOM 構造(シンタックスハイライト付き)を示すとても親切なエラーメッセージが表示され、デバッグの際に役立ちます。
このため、アサーションが失敗することはありません(アサーションが失敗する前に、クエリがスローするからです)。

このような理由から、多くの人はアサーションを省略します。
正直なところ、それはそれで構わないのですが、リファクタリング後に古いクエリが残ってしまっていると勘違いさせることなく、存在することを明示的に検証しているということをコードを読む人に伝えるために、個人的には、通常はアサーションを入れています。

アドバイス

存在していることを検証したいのであれば、明示する。

結論

Testing Library ファミリーのメンテナとして、私たちは人々ができるだけ効果的に使用できるような API を作るために最善を尽くし、それが不十分な場合には正確に文書化するようにしています。
しかし、これは本当に難しいことです(とりわけ API の変更や改善など)。
この記事があなたのお役に立てれば幸いです。
私たちは、皆さんが自信を持ってソフトウェアを出荷できるようになることを願っています。

幸運を!

69
42
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?