React Testing Libraryで基本的なReactコンポーネントをテストする使い方を説明します。
- React Testing Libraryとは
- Reactコンポーネントをレンダーして要素をテストする方法
- 要素を取得する方法
- Reactコンポーネントのインタラクションテストをする方法
- APIから非同期でデータ取得するコンポーネントをテストする
- React Hooksのテスト方法
- スナップショットテスト
- より高度なことをしたい場合
React Testing Libraryとは
- React Testing Libraryは、Reactコンポーネントをテストするためのシンプルなユーティリティです。
- テストユーティリティなので、Jestなどのテスティングフレームワークと組み合わせて使用すると良いでしょう。
- Testing Library では、テストしているコンポーネントの内部実装をテストしないように推奨しており、これにより保守性の高いテストを書けます。
- 補足:コンポーネントの内部実装の例(コンポーネントの内部状態、コンポーネントの内部メソッド、コンポーネントのライフサイクルメソッド、子コンポーネント)
(確認したバージョン 15.0.2 です)
Reactコンポーネントをレンダーして要素をテストする方法
与えられたpropsに対してコンポーネントが正しくレンダリングされるかをテストしたいことはよくあります。
render
でコンポーネントをレンダリング、screen
のクエリ系のメソッドで要素を取得し、expect
でアサーションをします。
また、@testing-library/jest-domを使うことで、DOM要素に対する便利なマッチャーを使えます。よく使うのは、次のようなメソッドです。
メソッド | 説明 |
---|---|
toBeInTheDocument() | 要素が存在するか |
toHaveTextContent(...) | 要素のテキストが一致するか |
toHaveAttribute(...) | 要素の属性が一致するか |
// hello.test.jsx import React from 'react' import '@testing-library/jest-dom'; import { render, screen } from "@testing-library/react"; import Hello from "./hello"; describe('Hello', () => { it('renders "Hey, stranger"', () => { // renderでコンポーネントをレンダリング render(<Hello />); // screen経由で要素を取得し、アサーションをする expect(screen.getByText('Hey, stranger')).toBeInTheDocument(); }); it('renders "Hello, John" with the name props', () => { render(<Hello name="John" />); expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Hello, John'); }); });
// hello.jsx - テスト対象のコンポーネント import React from "react"; export default function Hello(props) { if (props.name) { return <h1>Hello, {props.name}!</h1>; } else { return <span>Hey, stranger</span>; } }
要素を取得する方法
React Testing Libraryで要素を取得するために、
getBy
(複数の要素の場合はgetAllBy
)findBy
(複数の要素の場合はfindAllBy
)queryBy
(複数の要素の場合はqueryAllBy
)
また、要素の取得方法も、次のように様々な方法がサポートされています。
Role
:role属性で要素を取得LabelText
: ラベルのテキストで要素を取得PlaceholderText
: プレースホルダーのテキストで要素を取得Text
: テキストで要素を取得DisplayValue
:表示されている値で要素を取得AltText
:alt属性で要素を取得Title
:title属性で要素を取得。SVGなどで利用TestId
:data-testid属性で要素を取得:
これらを組み合わせて、getByRole
やqueryAllByText
など目的に合わせて使い分けることができます。
getBy
/ findBy
/ queryBy
の使い分けとしては、要素がある場合は、getBy
、要素を待つ場合はfindBy
、要素がない場合はqueryBy
を使うと良いでしょう。
またgetAllBy
/ findAllBy
/ queryAllBy
は、複数の要素を取得する場合に使います。こちらも同様に、要素がある場合は、getAllBy
、要素を待つ場合はfindAllBy
、要素がない場合はqueryAllBy
を使うと良いでしょう。
各メソッドの早見表は次のとおりです。
メソッド | 要素が0件 | 要素が1件 | 要素が複数 | Waitするか? |
---|---|---|---|---|
getBy | エラー | 1要素 | エラー | No |
findBy | エラー | 1要素 | エラー | Yes |
queryBy | null |
1要素 | エラー | No |
getAllBy | エラー | 配列 | 配列 | No |
findAllBy | エラー | 配列 | 配列 | Yes |
queryAllBy | [] | 配列 | 配列 | No |
Reactコンポーネントのインタラクションテストをする方法
コンポーネントに対して、ユーザー操作をテストしたいこともよくあります。@testing-library/user-eventを使うことでユーザー操作をシミュレートすることができます。(他の方法もあります)
import React from 'react'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event' import { render, screen } from "@testing-library/react"; import Toggle from "./toggle"; describe('Toggle', () => { it('renders "Turn on" button', () => { const onChange = jest.fn(); render(<Toggle onChange={onChange} />); expect(screen.getByRole('button')).toHaveTextContent('Turn on'); expect(onChange).not.toHaveBeenCalled(); }); it('renders "Turn off" button after click and called onChange', async () => { const onChange = jest.fn(); // userEvent.setup()でユーザーイベントをセットアップ // コンポーネントをレンダリングする前に呼び出すのを推奨 const user = userEvent.setup(); render(<Toggle onChange={onChange} />); const button = screen.getByRole('button') // ボタンクリックをシミュレート await user.click(button) expect(button).toHaveTextContent('Turn off'); expect(onChange).toHaveBeenCalledWith(true); }); // ... });
// toggle.jsx - テスト対象のコンポーネント import React, { useState } from "react"; export default function Toggle({ onChange }) { const [state, setState] = useState(false); return ( <button onClick={() => { setState(previousState => !previousState); onChange(!state); }} > {state === true ? "Turn off" : "Turn on"} </button> ); }
APIから非同期でデータ取得するコンポーネントをテストする
APIからデータを非同期で取得して、その内容を表示するコンポーネントはよくあります。このようなコンポーネントをテストする場合、jest.mock
を使ってAPIのレスポンスをモック化し、waitFor
を使って非同期処理が終わるまで待つことができます。
// posts.tests.jsx import React from 'react' import '@testing-library/jest-dom'; import { render, screen, waitFor } from "@testing-library/react"; import axios from 'axios'; import Posts from './posts'; jest.mock('axios'); describe('Posts', () => { it('renders the posts', async () => { // axios.getをモック化 const fakePosts = [ { id: 1, title: 'First post' }, { id: 2, title: 'Second post' }, ]; axios.get.mockImplementation(() => Promise.resolve({ json: () => Promise.resolve(fakePosts) }) ); render(<Posts />); // waitForで"loading..."が表示されなくなることを待つ await waitFor(() => { expect(screen.queryByText(/loading.../)).not.toBeInTheDocument(); }); expect(screen.getByText('First post')).toBeInTheDocument(); expect(screen.getByText('Second post')).toBeInTheDocument(); }); });
// posts.jsx - テスト対象のコンポーネント import React, { useState, useEffect } from "react"; import axios from "axios"; export default function Posts() { const [posts, setPosts] = useState([]); const [isLoading, setIsLoading] = useState(true); const fetchPosts = async () => { const response = await axios.get('/posts').then((res) => res.json()); console.log(response); setPosts(response); setIsLoading(false); }; useEffect(() => { fetchPosts(); }, []); if (isLoading) { return "loading..."; } return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }
React Hooksのテスト方法
React Hooksをテストしたい場合は、renderHook
を使います。renderHook
はHooksをテストするためのユーティリティです。
// use-logged-in-user.test.js import { renderHook } from '@testing-library/react'; import { waitFor } from "@testing-library/react"; import axios from 'axios'; import useLoggedInUser from './use-logged-in-user'; // Jestでaxiosをモック化 jest.mock('axios'); describe('useLoggedInUser', () => { it('returns logged in user when logged in', async () => { axios.get.mockResolvedValue({ name: 'Alice' }); // renderHookでHooksを実行 const { result } = renderHook(() => useLoggedInUser()); // Hooksの処理が非同期なので処理が終わるまでwaitForで待つ await waitFor(() => { expect(result.current.isLoading).toEqual(false) }); expect(result.current.user).toEqual({ name: 'Alice' }); }); it('returns null when not logged in', async () => { axios.get.mockResolvedValue(null); // renderHookでHooksを実行 const { result } = renderHook(() => useLoggedInUser()); // Hooksの処理が非同期なので処理が終わるまでwaitForで待つ await waitFor(() => { expect(result.current.isLoading).toEqual(false) }); expect(result.current.user).toEqual(null); }); });
// use-logged-in-user.js import { useState, useEffect } from 'react'; import axios from 'axios'; export default function useLoggedInUser() { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { axios.get('/api/user') .then((data) => { setUser(data) setIsLoading(false) }) .catch(() => { setIsLoading(false) }); }, []); return { isLoading, user }; };
参考: renderHookのAPI仕様
スナップショットテスト
JestのtoMatchSnapshot
を使うことで、コンポーネントのレンダリング結果をスナップショットとして保存し、次回のテスト時にスナップショットと比較することができます。
// hello.test.jsx import React from 'react' import '@testing-library/jest-dom'; import { render, screen } from "@testing-library/react"; import Hello from "./hello"; describe('Hello', () => { it('renders Hello component', () => { const { asFragment } = render(<Hello />); // スナップショットを作成 expect(asFragment()).toMatchSnapshot(); }) // ... });
作成されたスナップショットは次のようになります。今後Hello
コンポーネントを修正して、スナップショットと差分が出てくる場合は、スナップショットテストが失敗します。スナップショットの更新はjest --updateSnapshot
で行うことができます。
// __snapshots__/hello.test.jsx.snap // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Hello renders Hello component 1`] = ` <DocumentFragment> <span> Hey, stranger </span> </DocumentFragment> `;