UIコンポーネントのテストにおいて重要なのが、DOMノードを取得するためのクエリですが、正しく理解して使い分けられているでしょうか?
正しくクエリを使い分けないと、意図しないテスト結果になってしまったり、冗長なテストコードになってしまうため、UIコンポーネントテストを書く際にはぜひとも使い方を身に着けておきたいところです。
この記事では、最適なクエリを選択して正しいコンポーネントテストを書けるようになるため、Reactとreact-testing-libraryをベースとして、Testing Library
の3種類のDOMクエリの違いと、分かりづらいfindBy
とqueryBy
の使用例をご紹介します。
目次
テストが失敗するのは適切なクエリを使っていないからかも?
正しくテスト結果を得られない場合があると言いましたが、どんな場合に起こりうるのでしょうか?
例えば、コンポーネントのマウント時にで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)
getBy
とwaitFor
を組み合わせたメソッドで、条件に合致したノードの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
と組み合わせて、ノードが削除されるのを待つ際にも利用できます。
各クエリの詳細については公式ドキュメントも参照してください
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(...)
でラップするように警告されることがあります。
そういう場合は、findBy
やwaitFor
などを使い、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種類のクエリ違いと、findBy
、queryBy
の使い所についてご紹介しました。
正しいコンポーネントテストを書くためには、欲しいノードを正しく取得できることが前提になりますので、適切にクエリを使いこなすことが重要です。この記事を足がかりとしてTesting Library
の公式ドキュメントも読んで頂くとより理解が深まるので、ぜひそちらも目を通してみてください。