広告

【react-testing-library】queryBy, findByはどう使えばいいのか?

2022年1月27日

UIコンポーネントのテストにおいて重要なのが、DOMノードを取得するためのクエリですが、正しく理解して使い分けられているでしょうか?

正しくクエリを使い分けないと、意図しないテスト結果になってしまったり、冗長なテストコードになってしまうため、UIコンポーネントテストを書く際にはぜひとも使い方を身に着けておきたいところです。

この記事では、最適なクエリを選択して正しいコンポーネントテストを書けるようになるため、Reactとreact-testing-libraryをベースとして、Testing Libraryの3種類のDOMクエリの違いと、分かりづらいfindByqueryByの使用例をご紹介します。

テストが失敗するのは適切なクエリを使っていないからかも?

正しくテスト結果を得られない場合があると言いましたが、どんな場合に起こりうるのでしょうか?

例えば、コンポーネントのマウント時にでAPIを呼び出し、レスポンス結果をstateに反映させる場合を考えてみます。APIの呼び出しを擬似的にsleepで再現し、1秒後にレスポンスが帰ってくるようなコンポーネントのテストをgetByを使って書いてみます。

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

const App: FC = () => {
	const [counter, setCounter] = useState(0)
        const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
	useEffect(() => {
		sleep(1000).then(() => setCounter(1))
	}, [])
	return {counter}
}

it('renders a counter', () => {
	const { getByText } = render(<App />)
	expect(getByText(1)).toBeInTheDocument()
})

getByTextでは、stateの変更を待ってくれないため、テストは失敗します。 

では、UIの更新を待ってからノードを取得するfindByTextに置き換えてみるとどうなるでしょうか?

it('renders a counter', async () => {
	const { findByText } = render(<App />)
	expect(await findByText(1)).toBeInTheDocument()
})

無事テストが成功しました。 このように、UIの変更を待ってからテストしないと、正しい結果を得られない場合があります。

react testing libraryのクエリは場合によって使い分ける

getBy(getAllBy)

条件に合致したノードを返します。ノードが見つからない場合は例外をスローします。 最も基本となるクエリなので、特に理由がなければgetByを使用します。

findBy(findAllBy)

getBywaitForを組み合わせたメソッドで、条件に合致したノードのPromiseを返します。1000ms以内にノードが見つからなければ拒否(reject)されます。 したがって、下記の2種類のコードは同じになります。

await waitFor(()=> expect(getByRole('button')).toBeInTheDocument())

expect(await findByRole('button')).toBeInTheDocument()

findBy が使用されるのは、UIの更新を待ってからノードを取得する場合です。 描画に時間のかかるコンポーネントや、stateやpropsの変更によるコンポーネントが再描画を待つなど、getByと並んで活用頻度の高いクエリです。

queryBy(queryAllBy)

getByと同様に、条件に合致したノードを返しますが、見つからない場合はエラーを投げずにnullを返すため、存在しないノードに対して利用されます。

「存在しないノードを取得する必要があるのか?」と思うかもしれませんが、条件によって表示・非表示の切り替えや、一定時間で消えるアラートなどに対して、期待したとおりにノードが消えるかどうかをテストする際に活躍します

また、Testing Libraryの非同期メソッドwaitForElementToBeRemovedと組み合わせて、ノードが削除されるのを待つ際にも利用できます。

各クエリの詳細については公式ドキュメントも参照してください

出典:Testing Library(https://testing-library.com/docs/queries/about/#types-of-queries

findByはUIが更新された後のノードを取得に使う

findByは、stateやpropsの更新などに伴うUIの更新を待ってからノードを取得する際に利用します。

したがって、下記のようにボタンをクリックすると特定の文字列(下記の例ではLoading...)が表示されるテストケースの場合、クリック後に対象の文字列が表示されるまで待つ際などに活躍します。

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

if('should button disabled after click', async ()=>{
  render(<LoadingButton />)
  fireEvent.click(screen.getByRole('button'))
  expect(await screen.findByText('Loading...')).toBeInTheDocument()
  expect(await screen.findByRole('button')).toBeDisabled()
})

または、コンポーネントのマウント直後に、APIから取得したファイル一覧を表示するというような場合でもfindByの出番です。

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

const MyFileList: FC = () =>{
  const [myFiles, setMyFiles] = useState<string[]>([])
  useEffect(()=>{
    const files = await axios.get('http://...')
    setFiles(files)
  }, [])
  return(
    <>
    {files.map((file)=> (
      <li key={file.name}>{file.name} - {file.size}</li>
    ))}
    </>
  )
}
if('should render files stored on the storage', async ()=>{
  render(<MyFileList />)
  expect(await screen.findByText('myfile.bin',{},{timeout:3000})).toBeInTheDocument()
})

UIの変更を待たずにテストした場合、正しく結果が得られず、下記のようにstateの更新はact(...)でラップするように警告されることがあります。 

そういう場合は、findBywaitForなどを使い、UIの変更を待ちましょう。

なぜこの警告がされるのかは、こちらのサイトで詳しく解説されていますので、気になった方は確認してみてください。

queryByは存在しないノードの取得に使う

存在しないノードに対して使うqueryByは、他のクエリに比べると用途が限られていますが、下記のようにノードが非表示になるテストをしたい場合に活躍します。

import { fireEvent, screen, render, waitForElementToBeRemoved } from '@testing-library/react'
it('should dialog disappears after click cancel button',() => {
  render(<MyForm >)
  fireEvent.click(screen.getByText('Open Dialog'))
  expect(await findByRole('dialog')).toBeInTheDocument()
  fireEvent.click(screen.getByText('Cancel'))
  await waitForElementToBeRemoved(() => screen.queryByRole("dialog"));
})

この例では、waitForElementToBeRemoveメソッドと組み合わせることで、クエリ結果がnullになるまで待っています。

また、下記のようにstateの状態(下記の例ではユーザーのサインイン状態)に応じて表示、非表示をテストしたい際にも活躍します。

import { render, screen } from '@testing-library/react'
it('should render email when user is signed in', () => {
  render(<Header clientPrincipal={cp} />)
  expect(screen.getByText('user@gmail.com')).toBeInTheDocument()
})
it('should not render email when user is signed out', () => {
  render(<Header clientPrincipal={undefined} />)
  expect(screen.queryByText('user@gmail.com')).toBeNull()
})

まとめ

クエリの使い所

  • getBy:通常のノード取得
  • findBy:UIの更新を待ってからノードを取得
  • queryBy:存在しないノードを取得

今回は、Testing Libraryの3種類のクエリ違いと、findByqueryByの使い所についてご紹介しました。

正しいコンポーネントテストを書くためには、欲しいノードを正しく取得できることが前提になりますので、適切にクエリを使いこなすことが重要です。この記事を足がかりとしてTesting Library の公式ドキュメントも読んで頂くとより理解が深まるので、ぜひそちらも目を通してみてください。