JestとReact Testing Library(RTL)入門
このドキュメントでは、JestとReact Testing Library(RTL)を使用したReactコンポーネントのテストの基本について解説します。
1. Jestとは?
Jestは、Facebookによって開発されたJavaScriptのテストフレームワークです。特にReactアプリケーションのテストで広く利用されています。
主な特徴
- ゼロコンフィグ: 多くのプロジェクトで設定不要で動作します。
- スナップショットテスト: UIコンポーネントの変更を簡単に追跡できます。
- モック機能: 外部モジュールやAPIを簡単にモックできます。
- 高速な実行: テストを並列で実行し、高速なフィードバックを提供します。
インストール
npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react
設定
プロジェクトのルートに jest.config.js
を作成します。
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.(js|jsx)$': 'babel-jest',
},
};
また、Babelの設定ファイル .babelrc
も必要です。
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
簡単なテストの例
sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
2. React Testing Library(RTL)とは?
React Testing Libraryは、ユーザーの視点に立ったテストを推奨するテストライブラリです。コンポーネントの内部実装ではなく、ユーザーがどのようにコンポーネントを操作するかに焦点を当てます。
主な特徴
-
ユーザー中心のテスト:
getByText
,getByRole
など、ユーザーがUIをどのように認識するかに基づいたクエリを提供します。 - 実装の詳細を避ける: コンポーネントの内部状態やpropsに直接アクセスすることを避け、より堅牢なテストを作成できます。
- アクセシビリティ: アクセシビリティを意識したクエリが用意されており、自然とアクセシブルなコンポーネント開発につながります。
インストール
npm install --save-dev @testing-library/react @testing-library/jest-dom
@testing-library/jest-dom
は、DOMの状態を検証するためのカスタムマッチャーを提供します(例: toBeInTheDocument()
)。
設定
jest.config.js
に setupFilesAfterEnv
を追加して、カスタムマッチャーを読み込みます。
// jest.config.js
module.exports = {
// ...他の設定
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
};
setupTests.js
import '@testing-library/jest-dom';
3. Reactコンポーネントのテスト
Button
コンポーネントを例に、RTLを使ったテストの書き方を見ていきましょう。
Button.js
import React from 'react';
const Button = ({ onClick, children }) => {
return (
<button onClick={onClick}>
{children}
</button>
);
};
export default Button;
Button.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button component', () => {
test('renders button with text', () => {
render(<Button>Click me</Button>);
// `screen.getByText` を使ってテキストで要素を取得
const buttonElement = screen.getByText(/click me/i);
expect(buttonElement).toBeInTheDocument();
});
test('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
const buttonElement = screen.getByText(/click me/i);
// `fireEvent` を使ってクリックイベントを発生させる
fireEvent.click(buttonElement);
// `handleClick` が1回呼び出されたことを確認
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
主なRTLのAPI
-
render()
: コンポーネントをDOMにレンダリングします。 -
screen
: レンダリングされたDOMにアクセスするためのクエリを提供します。 -
fireEvent
: クリックや入力などのユーザーイベントを発生させます。 -
jest.fn()
: モック関数を作成し、関数の呼び出しを追跡します。
4. JestとRTLの役割分担
JestとRTLは競合するものではなく、協調して動作します。それぞれの役割を理解することが重要です。
-
Jest:
- テストランナー: テストファイルを見つけて実行し、結果を報告します。
-
アサーションライブラリ:
expect()
や.toBe()
などの関数を提供し、値が期待通りか検証します。 -
モック機能:
jest.fn()
やjest.mock()
を提供し、関数やモジュールを偽のバージョンに置き換えます。テスト環境をコントロールするために不可欠です。
-
React Testing Library (RTL):
-
コンポーネントのレンダリング:
render()
関数でテスト対象のコンポーネントを仮想DOMに描画します。 -
DOMクエリ:
screen.getByRole()
のように、ユーザーがUIをどのように認識するかに基づいて要素を検索する手段を提供します。 -
ユーザーイベントの発火:
fireEvent
や@testing-library/user-event
を使って、クリックやキーボード入力などのユーザー操作をシミュレートします。
-
コンポーネントのレンダリング:
結論: Jestがテスト全体のフレームワーク(実行、検証、モック)を提供し、RTLはその上でReactコンポーネントをユーザー視点でテストするための便利なツールセットを提供する、という関係性です。
5. より実践的なテストシナリオ
シナリオ1: フォームの入力と送信
ユーザーがフォームに入力し、送信ボタンを押すという一連の操作をテストします。
LoginForm.js
import React, { useState } from 'react';
const LoginForm = ({ onSubmit }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">Login</button>
</form>
);
};
export default LoginForm;
LoginForm.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
// fireEventよりもユーザーの操作をより忠実に再現するuser-eventの利用を推奨
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('allows the user to log in', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// userEventを使って入力操作をシミュレート
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'password');
// 送信ボタンをクリック
await userEvent.click(screen.getByRole('button', { name: /login/i }));
// onSubmitが正しい引数で呼び出されたか検証
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password',
});
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
ポイント: fireEvent
よりも @testing-library/user-event
を使うと、実際のユーザー操作(キー入力のタイミングなど)をより正確にシミュレートでき、推奨されています。
シナリオ2: API通信と非同期処理
コンポーネントがマウントされた時にAPIを叩き、取得したデータを表示するケースです。ここではaxios
を使ったAPI通信をjest.mock()
でモックします。
UserProfile.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
const response = await axios.get(`https://api.example.com/users/${userId}`);
setUser(response.data);
} catch (error) {
console.error('Failed to fetch user');
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
return <div>User not found</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
export default UserProfile;
UserProfile.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import UserProfile from './UserProfile';
// axiosモジュール全体をモック化
jest.mock('axios');
test('fetches and displays user data', async () => {
const mockUser = { name: 'John Doe', email: 'john.doe@example.com' };
// モックされたaxios.getが解決する値を設定
axios.get.mockResolvedValue({ data: mockUser });
render(<UserProfile userId="1" />);
// 初期表示(ローディング)の確認
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// `findBy`系のクエリは非同期で要素が出現するのを待つ
const userName = await screen.findByText('John Doe');
expect(userName).toBeInTheDocument();
// `getBy`は同期的。この時点では要素は存在しているはず
expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
// ローディング表示が消えたことを確認
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
ポイント:
-
jest.mock('axios')
でaxios
からのimport
をモックに差し替えます。 -
mockResolvedValue
で非同期APIの成功レスポンスを定義します。 -
findByText
やwaitFor
ユーティリティを使い、非同期でUIが更新されるのを待ちます。getBy
は同期的、queryBy
は要素がなくてもエラーを投げない、findBy
は非同期で待つ、という違いがあります。
まとめ
JestとReact Testing Libraryを組み合わせることで、単純なUIの表示からフォーム操作、非同期通信といった複雑なシナリオまで、Reactアプリケーションの品質を高く保つための堅牢なテストスイートを構築できます。ユーザーの操作をシミュレートすることで、リファクタリングに強く、信頼性の高いテストを書くことを目指しましょう。