Reactのユニットテストでよく使われるライブラリにenzyme
があります。
enzyme
を紹介する記事を見ているとshallow
が旨味の一つであり、簡単に単体テストが出来るとよく紹介されるのを見かけます。
Jestと用いられる、enzyme
以外のライブラリもいくつかあるようなので、人気な理由とその使い方を備忘録がてら紹介しようと思います。
Jest
まずはJestについて簡単に特徴を紹介します。
公式サイト:https://jestjs.io/docs/ja/tutorial-react
- facebook製のテストフレームワーク
- create-react-appでデフォルトで入っている
- React Component のユニットテストに使える
- Unit Tests + Integration Tests
- アサーションライブラリ(Mocha + Chai → Jest)
普段下のようなテストをすることはありませんが、「こんな風に書くんだな〜」と参考程度にソースコードを紹介しておきます。
test("Fake Test", () => {
expect(true).toBeTruthy() // ← PASS
})
test("Fake Two", () => {
expect(false).toBeTruthy() // ← FAILED
})
上はかなりシンプルなテストです。もう一つ例ですが、例えば以下のようなApp.jsを作ったとして、add
関数のテストをしたい時は
export const add = (x, y) => {
return x + y
}
import { add } from './App'
test('add', () => {
expect(add(1, 2)).toBe(3) // PASS
expect(add(5, 2)).toBe(7) // PASS
})
のようにしてテストすることができます。
expect
を使うことになりますが、使える関数一覧は👉こちらから VSCodeで補完が効くので、便利です。
react-test-renderer
Facebook公式のライブラリです。特徴は以下の通りです。
- React コンポーネントをピュアな JavaScript オブジェクトにレンダーすることができる React レンダラを提供
- スナップショットテストで使用
- 出力を走査して特定のノードを検索し、それらに対してアサーションを行うこともできます
公式サイト:https://www.npmjs.com/package/react-test-renderer
React公式ドキュメント「Test Renderer」:https://ja.reactjs.org/docs/test-renderer.html:
テストにスナップショットを使いたい時に登場するライブラリのイメージでしたが、enzymeのよう単体テストに使うことも出来るので、使ってみることにします。
検証したいテスト項目
検索窓で使用する<InputSearchWord />
コンポーネントについてテストをしたいと思います。
テストするのは以下の通り、
- スナップショットテスト
- 1文字入力すると検索ボタンが非活性な状態からアクティブになる
- 30文字入力するとエラーメッセージは表示されない
主に、入力文字数と入力文字に関するエラーメッセージと、正しくボタンがアクティブ・非活性の表現が出来ているかテストしたいと思います。
実際のコード
スナップショットテスト
スナップショットは
const tree = renderer.create(<コンポーネント).toJSON()
-
expect(tree).toMatchSnapshot()
でスナップショットを作ることができます。テストを走らせると同時に「snapshots」フォルダが自動で作成され、toMatchSnapshot()
した瞬間のDOMを生成してくれます。
import React from "react"
import renderer from "react-test-renderer"
import Button from "@material-ui/core/Button"
import InputSearchWord from "components/InputSearchWord"
describe("<InputSearchWord />", () => {
// ここにテストを書いていく
// スナップショットテスト
it("スナップショットテスト", () => {
const props = {
onSubmit: () => {},
}
const tree = renderer.create(<InputSearchWord {...props} />).toJSON()
expect(tree).toMatchSnapshot()
})
})
1文字入力すると検索ボタンが非活性な状態からアクティブになる
次は実際にテストしたいコンポーネントを作成して、コンポーネント内にあるButton
タグ、テキストフィールドにそれぞれイベントを発火させたいと思います。
Button
はMaterial-UIを使用しているため、上記のソースコードのようにimportしています。(import Button from "@material-ui/core/Button"
)
- コンポーネントの生成は
renderer.create
- ①
const testRenderer = renderer.create(コンポーネント)
- ②
const testInstance = testRenderer.root
- ①
- コンポーネント内にあるButtonの取得は
findByType
- Buttonタグのpropsの値をテストしたい時は
props.props名
- イベント発火は
.props.onChange({ target: { value: "ダミーテキスト" } })
以上を踏まえ、このように書きます。
it("1文字入力すると検索ボタンが非活性な状態からアクティブになる", () => {
const props = {
onSubmit: () => {},
}
const testRenderer = renderer.create(<InputSearchWord {...props} />)
const testInstance = testRenderer.root
// デフォルト表示
// 検索エリア未入力時は検索ボタンは非活性
expect(testInstance.findByType(Button).props.disabled).toBe(true)
// 1文字入力する
testInstance
.findByType(TextField)
.props.onChange({ target: { value: "a" } })
// 検索エリア未入力時は検索ボタンがアクティブになる
expect(testInstance.findByType(Button).props.disabled).toBe(false)
})
今回は入力文字数制限「30文字」とした時、エラーメッセージを表示させたいとします。
// 31文字入力する
testInstance.findByType(TextField).props.onChange({
target: {
value: "1234567890123456789012345678901",
},
})
// 文字数制限のエラーメッセージが表示される
expect(testInstance.findByType(TextField).props.errorMessage).toBe(
"検索キーワードは30文字以下である必要があります"
)
errorMessage
はpropsで渡す仕様にしていたので、上記のように書いています。
これでテストはALL PASSです!
Enzyme
Airbnb製ののライブラリで、使い勝手が良いためか、Jest+enzymeの2コンビは人気なように見受けます。特徴は以下の通りです。
- Reactでのユニットテストを助けてくれるテストユーティリティツール
- airbnb製
- テスト用のバーチャルDOMを作ったり、DOM内の要素を探してくれる
- シンプルなイベントをシュミレートしてくれる
- 「shallow(浅い)」レンダリングで子コンポーネントをネストしている親コンポーネントを無視してレンダリングしてくれる → コンポーネント単体でテストができる
「Jest + enzyme」で紹介されている記事が多いため、問題にぶつかった時解決しやすいかもしれません。
早速使ってみます!
検証したいテスト項目
App.js内にモーダル用のコンポーネントをimportするとします。
モーダル表示はApp.jsのローカルなステートopen
のtrue/false
で制御しており、モーダルのキャンセルボタンが正しく動作するかテストしていきます。
実際のコード
enzymeのshallow
を紹介つもりでしたが、Material-UIが提供しているcreateShallow
で実装していきます。他の使い方はenzymeと同じです。
-
enzyme-adapter-react-16
の併用が必要 -
shallow
でコンポーネントを作成する - コンポーネント内のエレメントは
find
で取得 - イベント発火は
.simulate("イベント")
-
setProps
でステートの値をセットできる
import React from "react"
import Enzyme from "enzyme"
import EnzymeAdapter from "enzyme-adapter-react-16"
import { createShallow } from "@material-ui/core/test-utils"
import App from "components/App"
Enzyme.configure({
adapter: new EnzymeAdapter(),
disableLifecycleMethods: true,
})
describe("<App />", () => {
let shallow
const props = {
onClose: jest.fn(),
}
beforeAll(() => {
shallow = createShallow({ dive: true })
})
it("「キャンセル」ボタンをクリックするとモーダルが消える", () => {
const wrapper = shallow(<App {...props} />)
// 最初にモーダルを開いた状態にする
wrapper.setProps({ open: true })
// 「キャンセル」ボタンのクリックシュミレート
wrapper.find("[data-test='cancelButton']").simulate("click")
// `close`functionが呼ばれているか確認
expect(props.onClose).toHaveBeenCalled()
})
})
こちらもテストはALL PASSでしたが、選んだコンポーネントが悪かったのか、実装にかなり悩まされました。。
react-testing-library
こちらはenzymeと似たような使い方ができるライブラリです。Jest + enzyme
よりは記事数が少ないものの、Jest + react-testing-library
コンビは人気が出てきているように見受けます。
「ユーザーが実際に操作するイベントに沿ってテストする」を掲げたライブラリなので、まだ触って間もないですが使い勝手が良いです。
検証したいテスト項目
テキストフィールドコンポーネントを検証しようと思います。
このコンポーネントでは以下、
- ①テキスト入力エリア
- ②残り何文字入力できるか、残文字数表示
を表現するようになっています。
実際のコード
react-testing-libraryはざっくりと、以下のような実装が可能です。
-
"jest-dom/extend-expect"
の読み込みが必要 -
react-testing-library
はイベント処理の発火にfireEvent
を提供している - 同様にレンダリングしたDOMのunmountに
cleanup
- スナップショットを使いたい時はasFragment
import TextField from "components/TextField"
import "jest-dom/extend-expect"
import React from "react"
import { cleanup, fireEvent, render } from "react-testing-library"
describe("<TextField />", () => {
afterEach(cleanup)
it("スナップショットテスト", () => {
const { asFragment } = render(<TextField />)
// スナップショットテスト
expect(asFragment()).toMatchSnapshot()
})
})
テキストエリアに入力するイベントを発火させる
こちらのQiitaの記事が非常に参考にさせていただきました!
→ 「フロントエンドでTDDを実践する(react-testing-libraryを使った実践編)」
こちらにある通り、本当ならgetByTestId
の多用は避けるべきとありますが、すみません、、まだ使い始めたばかりなのでこれしか使いませんでした。。
getByTestId
を使う場合、テスト対象となるエレメントに<div data-testid="foo">
のようにdata-testid
を付与する必要があります。
- テスト対象のエレメントに
data-testid="DUMMY"
を付与(避けるのが良い) - テストコードでまず
const { getByTestId } = render(コンポーネント)
-
getByTestId
関数を使って取得 const input = getByTestId("textField")
- テキストの値をテストしたい時は
toHaveTextContent()
it("入力文字数が表示される", () => {
const { getByTestId } = render(<TextField />)
// 入力エリア
const input = getByTestId("textField")
// カウント数表示
const count = getByTestId("counterNumber")
// デフォルトは空であることを確認
expect(input).toHaveTextContent("")
// デフォルト表示"140"
expect(count).toHaveTextContent("140")
// テキストを1文字入力する
input.innerHTML = "a"
fireEvent.input(input)
expect(input).toHaveTextContent(input.innerHTML)
// カウント数が"140"から"139"に変更、且つ、エラーは表示されていない
expect(count).toHaveTextContent("139")
// テキストを200文字入力する
input.innerHTML =
"ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。ここには200文字のテキストが入ります。"
fireEvent.input(input)
expect(input).toHaveTextContent(input.innerHTML)
// エラー表示される
expect(count).toHaveTextContent("-45")
})
イベント処理のところ、今回input
イベントですが、恐らくよく使われるイベントは
fireEvent.click(getByText("Up"))
とか
fireEvent.change(getByPlaceholderText("placeholder"), {
target: { value: "new value" }
})
でしょうか。
感覚的にテストがしやすかったので、個人的にreact-testing-libraryの理解をさらに深めようと思っています。
終わりに
比較してみましたが、「結果これが良い!」と断定出来ていません^ ^;
「このライブラリのここが良かった」等ありましたら気軽にご意見ください!