こんにちは。Testing Library を布教したいマンの梅津です。
以前書いた記事で実装の詳細をテストすることの弊害と React Testing Library による解決方法を Kent から学びました。
今回はその React Testing Library の簡単な使い方を紹介したいと思います。
Testing Library には Vue Testing Library や Angular Testing Library などもあるので、普段 React を触っていない人も是非目を通してみてください!
サンプルとして次のようなコンポーネントを用意しました。
あまり意味のない処理も入っていますが、気にせずテストを書いていきましょう。
import React, { ChangeEvent, FormEvent, useState } from "react" import { updateProfile } from "../api" type Props = { title?: string name?: string } function UserProfileEditor({ title, name: initialName }: Props) { const [name, setName] = useState(initialName ?? "") const [inputValue, setInputValue] = useState("") const [hasError, setHasError] = useState(false) const handleChange = (e: ChangeEvent<HTMLInputElement>) => { setInputValue(e.target.value) } const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() setHasError(false) try { const { userName } = await updateProfile(inputValue) setName(userName) setInputValue("") } catch (e) { setHasError(true) } } return ( <form onSubmit={handleSubmit}> <h1>{title ?? "Edit Your Profile"}</h1> <div>Current Name: {name}</div> <label htmlFor="name-input">Name</label> <input id="name-input" value={inputValue} onChange={handleChange} /> <button type="submit">Submit</button> {hasError && <div role={"alert"}>update error!!</div>} </form> ) } export { UserProfileEditor }
まずは何からはじめればいいの?
必要なものをインストールしましょう。
jest を入れたらこちらを参考にReact Testing Library のインストールを行ってください。
npm install --save-dev @testing-library/react
それと jest-dom のカスタムマッチャーがとても便利なので、こちらも入れておくことをオススメします。
jest-dom を入れたあとは jest.config.js に次のような設定をしておくとカスタムマッチャーの import を省略できます。
module.exports = { ... setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'] }
ESLint を入れているなら plugin の設定をしておくのもいいでしょう。
テストを書き始めるには?
まずは render 関数を使ってコンポーネントを描画しましょう。
UI テストを行うにはこれがないと始まりません。
import React from "react" import { render } from "@testing-library/react" import { UserProfileEditor } from "./UserProfileEditor" test("プロフィール編集フォームのレンダリング", () => { render(<UserProfileEditor />) })
要素を取得したい
getByLabelText や getByText といった getBy* 関数を使いましょう。
React Testing Library では、この getBy* 関数を使うことが基本になります。
これらの関数は render 関数の戻り値から取得できます。
import React from "react" import { render } from "@testing-library/react" import { UserProfileEditor } from "./UserProfileEditor" test("プロフィール編集フォームのレンダリング", () => { const { getByLabelText, getByText } = render(<UserProfileEditor />) // 第1引数に渡した値にマッチする要素を返す const heading = getByText("Edit Your Profile") expect(heading).toBeInTheDocument() // getBy* 関数はマッチする要素がないときにエラーを投げる // document内に該当の要素があることを確認したいだけならこれでも効果はある getByText("Edit Your Profile") // 正規表現を渡すことも可能 const nameInput = getByLabelText(/name/i) as HTMLInputElement nameInput.value = "umetsu" expect(nameInput).toHaveValue("umetsu") // ボタンの取得には getByText が使える getByText(/submit/i) })
getBy* 関数はマッチする要素がない場合にエラーを投げます。
つまり、これらの関数はある要素が存在することを確認するための暗黙的なアサーションにもなっています。
加えて複数の要素がマッチした場合もエラーを投げます。
複数の要素を扱いたい場合は getAllBy* 関数を使ってください。
他の getBy* 関数や、引数として渡すマッチャーの詳細は次のドキュメントをご参照ください。
描画している DOM の内容を知りたい
debug 関数が用意されているのでこれを使いましょう。
debug 関数は呼び出した時点の DOM の状態を記録しログに出力してくれます。
また、特定の要素を渡せばそれを出力してくれます。
import React from "react" import { render } from "@testing-library/react" import { UserProfileEditor } from "./UserProfileEditor" test("プロフィール編集フォームのレンダリング", () => { const { getByText, debug } = render(<UserProfileEditor />) debug() const submitButton = getByText(/submit/i) debug(submitButton) })
出力結果
● Console
console.log node_modules/@testing-library/react/dist/pure.js:94
<body>
<div>
<form>
<h1>
Edit Your Profile
</h1>
<div>
Current Name:
</div>
<label
for="name-input"
>
Name
</label>
<input
id="name-input"
value=""
/>
<button
type="submit"
>
Submit
</button>
</form>
</div>
</body>
console.log node_modules/@testing-library/react/dist/pure.js:94
<button
type="submit"
>
Submit
</button>
イベントを発火させたい
fireEvent を使いましょう。
fireEvent に用意されている change や click を使うことで各イベントを発火させることができます。
import React from "react" import { render, fireEvent } from "@testing-library/react" import { UserProfileEditor } from "./UserProfileEditor" test("プロフィール編集フォームのレンダリング", () => { const { getByLabelText, getByText } = render(<UserProfileEditor />) const nameInput = getByLabelText(/name/i) // onChangeの発火 fireEvent.change(nameInput, { target: { value: "umetsu" } }) expect(nameInput).toHaveValue("umetsu") const submitButton = getByText(/submit/i) // onClickの発火 fireEvent.click(submitButton) })
fireEvent の他に User Event Module というものもあります。
こちらのほうが抽象度の高いコードを書けるのでオススメです。
import React from "react" import { render } from "@testing-library/react" import user from "@testing-library/user-event" import { UserProfileEditor } from "./UserProfileEditor" test("プロフィール編集フォームのレンダリング", () => { const { getByLabelText, getByText } = render(<UserProfileEditor />) const nameInput = getByLabelText(/name/i) // targetやvalueといった文字が消え、「ユーザーが文字を入力する」ということをより表現したコードになる user.type(nameInput, "umetsu") expect(nameInput).toHaveValue("umetsu") const submitButton = getByText(/submit/i) user.click(submitButton) })
Props を変更して再描画したい
rerender 関数が用意されているのでこれを使いましょう。
import React from "react" import { render } from "@testing-library/react" import { UserProfileEditor } from "./UserProfileEditor" test("プロフィール編集フォームのレンダリング", () => { const { getByText, rerender } = render(<UserProfileEditor />) getByText(/edit your profile/i) rerender(<UserProfileEditor title={"Custom Title"} />) getByText(/custom title/i) })
要素が表示されていないことを確認したい
queryBy* 関数を使いましょう。
queryBy* 関数はマッチする要素がない場合に null を返します。getBy* のようにエラーにはなりません。
import React from "react" import { render } from "@testing-library/react" import { UserProfileEditor } from "./UserProfileEditor" test("プロフィール編集フォームのレンダリング", () => { const { queryByRole } = render(<UserProfileEditor />) expect(queryByRole("alert")).toBeNull() })
非同期処理を行うコンポーネントをテストしたい
findBy* 関数を使って非同期処理の終了を待ちましょう。
findBy* 関数は通信処理を含むコンポーネントでよく使います。
他にもアプリケーション全体のテストを書く場合、画面遷移やアニメーションが含まれることもあるでしょう。
こういったときも findBy* 関数を使うとテストが柔軟になり壊れにくくなります。
import React from "react" import { render } from "@testing-library/react" import user from "@testing-library/user-event" import { updateProfile } from "../api" import { UserProfileEditor } from "./UserProfileEditor" jest.mock("../api") const mockUpdateProfile = updateProfile as jest.Mock test("プロフィール編集フォームのレンダリング", async () => { mockUpdateProfile.mockResolvedValueOnce({ userName: "umetaro" }) const { getByLabelText, getByText, findByText } = render( <UserProfileEditor name={"umetsu"} /> ) // 現在の名前が表示されているか getByText(/current name: umetsu/i) await user.type(getByLabelText(/name/i), "umetaro") user.click(getByText(/submit/i)) // 通信処理が呼ばれているか確認 expect(mockUpdateProfile).toHaveBeenCalledWith("umetaro") expect(mockUpdateProfile).toBeCalledTimes(1) // 更新後の名前が表示されているか await findByText(/current name: umetaro/i) })
まとめ
いかがだったでしょうか。
今回は本当に基本的な部分のみの紹介となりましたが、これだけでも意味のある UI テストを書ける気がしませんか?
この記事によって Testing Library を使う際のハードルが下がれば幸いです。
最後におさらいをして終わりにしましょう。
- 基本は
getBy*,getAllBy*関数を使う - 表示されていないことを確認したければ
queryBy*,queryAllBy*関数を使う - 非同期処理やアニメーションがある場合は
findBy*,findAllBy*関数を使う
以上です!それでは、また。