Jest と Testing Library の違いと使い分け

フロントエンド開発において、テストツールの選択は品質の高いアプリケーションを構築する上で重要な決断の一つです。特にJestとTesting Libraryという二つの主要なツールについて、多くの開発者が「どちらを使うべきか」「併用できるのか」という疑問を抱いているのではないでしょうか。
この記事では、Jest と Testing Library の本質的な違いから実践的な使い分けまで、テストツール選択の決定版ガイドとして詳しく解説いたします。それぞれの特徴を理解し、プロジェクトに最適な選択ができるようになりましょう。
Jest と Testing Library の基本概念
Jest とは何か
Jest は、Meta(旧 Facebook)によって開発された JavaScript テスティングフレームワークです。テストランナーとして機能し、テストの実行環境を提供します。
Jest の主要機能
javascript// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Jest は以下の機能をオールインワンで提供しています:
# | 機能 | 説明 |
---|---|---|
1 | テストランナー | テストファイルの検出・実行 |
2 | アサーション | expect().toBe() などの検証機能 |
3 | モック機能 | 外部依存関係の偽実装 |
4 | カバレッジ測定 | コードカバレッジの算出 |
5 | スナップショット | UI の変更検知テスト |
Jest 単体の強み
javascript// Jest単体でのシンプルなテスト例
describe('計算関数のテスト', () => {
test('足し算が正しく動作する', () => {
const add = (a, b) => a + b;
expect(add(2, 3)).toBe(5);
});
test('非同期処理のテスト', async () => {
const fetchData = () => Promise.resolve('データ');
const result = await fetchData();
expect(result).toBe('データ');
});
});
Testing Library とは何か
Testing Library(主に React Testing Library)は、テストユーティリティライブラリです。「ユーザーがどのように使用するか」という観点からテストを記述するためのヘルパー関数を提供します。
Testing Library の設計思想
Testing Library は「実装の詳細ではなく、動作をテストする」という哲学に基づいています。
javascript// React Testing Libraryの基本例
import {
render,
screen,
fireEvent,
} from '@testing-library/react';
import '@testing-library/jest-dom';
test('ボタンクリックでテキストが変更される', () => {
const Button = () => {
const [text, setText] = useState('初期値');
return (
<button onClick={() => setText('変更後')}>
{text}
</button>
);
};
render(<Button />);
// ユーザーの視点でテスト
const button = screen.getByRole('button');
expect(button).toHaveTextContent('初期値');
fireEvent.click(button);
expect(button).toHaveTextContent('変更後');
});
Testing Library が提供する機能
# | 機能 | 説明 |
---|---|---|
1 | レンダリング | コンポーネントの仮想 DOM 生成 |
2 | クエリ関数 | DOM 要素の取得 |
3 | イベントシミュレーション | ユーザー操作の再現 |
4 | 非同期処理待機 | 状態変更の待機 |
5 | ユーザー中心 API | アクセシビリティを重視した要素取得 |
根本的な違いと役割分担
テストランナー vs テストユーティリティ
Jest と Testing Library の最も重要な違いは、それぞれが担う役割です。
Jest:テスト実行基盤
javascript// Jest設定ファイル例(jest.config.js)
module.exports = {
testEnvironment: 'jsdom', // ブラウザ環境をシミュレート
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
collectCoverage: true,
coverageDirectory: 'coverage',
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
],
};
Testing Library:テスト記述支援
javascript// setupTests.js でTesting Libraryを設定
import '@testing-library/jest-dom';
// カスタムマッチャーが使用可能になる
expect(element).toBeInTheDocument();
expect(element).toHaveTextContent('テキスト');
提供する機能の範囲
両者の機能範囲を明確に理解することで、適切な使い分けができるようになります。
Jest 単体でできること
javascript// Jest単体での一般的なテスト
describe('APIクライアントのテスト', () => {
// モック機能
const mockFetch = jest.fn();
global.fetch = mockFetch;
beforeEach(() => {
mockFetch.mockClear();
});
test('APIエラーハンドリング', async () => {
// エラーレスポンスをモック
mockFetch.mockRejectedValue(new Error('Network Error'));
const apiClient = new ApiClient();
await expect(apiClient.getData()).rejects.toThrow(
'Network Error'
);
});
});
Testing Library 単体の制限
javascript// Testing Libraryにはテストランナー機能がない
// これだけでは実行できない
import { render, screen } from '@testing-library/react';
// describe, test, expect などはJestが提供
// Testing Libraryだけでは使用不可
render(<MyComponent />);
const element = screen.getByText('テキスト');
// expect関数もJest由来
expect(element).toBeInTheDocument();
実際のエラー例:Testing Library 単体使用時
Testing Library だけをインストールしてテストを実行すると、以下のエラーが発生します:
vbnetReferenceError: describe is not defined
at Object.<anonymous> (src/App.test.js:5:1)
at Module._compile (internal/modules/cjs/loader.js:1063:30)
TypeError: expect is not defined
at Object.<anonymous> (src/App.test.js:12:5)
このエラーは、describe
やexpect
が Jest によって提供される関数であり、Testing Library には含まれていないことを示しています。
具体的な機能比較
アサーション機能
Jest のアサーション
javascriptdescribe('Jestアサーションの例', () => {
test('基本的なマッチャー', () => {
// 数値比較
expect(2 + 2).toBe(4);
expect(2.1 + 2.2).toBeCloseTo(4.3, 1);
// オブジェクト比較
expect({ name: 'John' }).toEqual({ name: 'John' });
// 配列・文字列
expect(['apple', 'banana']).toContain('apple');
expect('Hello World').toMatch(/World/);
// 例外テスト
expect(() => {
throw new Error('エラー');
}).toThrow('エラー');
});
});
Testing Library のアサーション拡張
javascriptimport '@testing-library/jest-dom';
test('DOM要素専用のアサーション', () => {
render(<input placeholder='名前を入力' disabled />);
const input = screen.getByPlaceholderText('名前を入力');
// Testing Libraryが拡張したマッチャー
expect(input).toBeInTheDocument();
expect(input).toBeDisabled();
expect(input).toHaveAttribute(
'placeholder',
'名前を入力'
);
// Jestの標準マッチャーも使用可能
expect(input.tagName).toBe('INPUT');
});
モック機能
Jest のモック機能
javascript// 関数モック
const mockCallback = jest.fn();
mockCallback('arg1', 'arg2');
expect(mockCallback).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockCallback).toHaveBeenCalledTimes(1);
// モジュールモック
jest.mock('axios', () => ({
get: jest.fn(() =>
Promise.resolve({ data: 'テストデータ' })
),
}));
// タイマーモック
jest.useFakeTimers();
jest.advanceTimersByTime(1000);
Testing Library とモックの組み合わせ
javascript// API呼び出しコンポーネントのテスト
jest.mock('../api/userApi', () => ({
fetchUser: jest.fn(),
}));
import { fetchUser } from '../api/userApi';
test('ユーザー情報を表示する', async () => {
// Jestでモック設定
fetchUser.mockResolvedValue({
id: 1,
name: '山田太郎',
});
// Testing Libraryでレンダリング
render(<UserProfile userId={1} />);
// 非同期処理を待機(Testing Library機能)
await waitFor(() => {
expect(
screen.getByText('山田太郎')
).toBeInTheDocument();
});
// モック呼び出しを検証(Jest機能)
expect(fetchUser).toHaveBeenCalledWith(1);
});
DOM 操作・クエリ機能
Jest での制限
Jest は基本的に DOM 操作機能を提供していません。DOM 要素の取得には標準の DOM API を使用する必要があります。
javascript// Jest単体でのDOM操作(制限的)
test('DOM操作の例', () => {
document.body.innerHTML = `
<div>
<button id="test-button">クリック</button>
</div>
`;
const button = document.getElementById('test-button');
expect(button.textContent).toBe('クリック');
// イベント発火も手動で実装
const event = new Event('click');
button.dispatchEvent(event);
});
Testing Library の強力なクエリ機能
javascripttest('Testing Libraryのクエリ機能', () => {
render(
<form>
<label htmlFor='username'>ユーザー名</label>
<input id='username' type='text' />
<button type='submit'>送信</button>
</form>
);
// 様々な取得方法
const input = screen.getByLabelText('ユーザー名');
const button = screen.getByRole('button', {
name: '送信',
});
const form = screen.getByRole('form');
// アクセシビリティを重視した取得
expect(input).toBeInTheDocument();
expect(button).toBeInTheDocument();
});
よくあるエラー:要素が見つからない場合
javascriptTestingLibraryElementError: Unable to find an element with the text: ログイン
Ignored nodes: comments, <script />, <style />
<body>
<div>
<button>
Login
</button>
</div>
</body>
このエラーは、日本語の「ログイン」というテキストを探しているが、実際には英語の「Login」になっている場合に発生します。
実際のコード例で見る使い分け
Jest 単体でのテスト例
ビジネスロジックのテスト
javascript// src/utils/calculator.js
export class Calculator {
add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('引数は数値である必要があります');
}
return a + b;
}
divide(a, b) {
if (b === 0) {
throw new Error('ゼロで割ることはできません');
}
return a / b;
}
}
// src/utils/calculator.test.js
import { Calculator } from './calculator';
describe('Calculator', () => {
let calculator;
beforeEach(() => {
calculator = new Calculator();
});
describe('add メソッド', () => {
test('正常な加算処理', () => {
expect(calculator.add(2, 3)).toBe(5);
expect(calculator.add(-1, 1)).toBe(0);
expect(calculator.add(0.1, 0.2)).toBeCloseTo(0.3);
});
test('無効な引数のエラーハンドリング', () => {
expect(() => calculator.add('2', 3)).toThrow(
'引数は数値である必要があります'
);
expect(() => calculator.add(null, 3)).toThrow();
});
});
describe('divide メソッド', () => {
test('正常な除算処理', () => {
expect(calculator.divide(10, 2)).toBe(5);
expect(calculator.divide(7, 3)).toBeCloseTo(2.333, 3);
});
test('ゼロ除算エラー', () => {
expect(() => calculator.divide(10, 0)).toThrow(
'ゼロで割ることはできません'
);
});
});
});
API クライアントのテスト
javascript// src/api/userApi.js
export class UserApi {
constructor(httpClient) {
this.httpClient = httpClient;
}
async getUser(id) {
try {
const response = await this.httpClient.get(
`/users/${id}`
);
return response.data;
} catch (error) {
if (error.response?.status === 404) {
throw new Error(
`ユーザーID ${id} が見つかりません`
);
}
throw new Error('ユーザー情報の取得に失敗しました');
}
}
}
// src/api/userApi.test.js
import { UserApi } from './userApi';
describe('UserApi', () => {
let userApi;
let mockHttpClient;
beforeEach(() => {
mockHttpClient = {
get: jest.fn(),
};
userApi = new UserApi(mockHttpClient);
});
test('ユーザー情報を正常に取得', async () => {
const userData = { id: 1, name: '山田太郎' };
mockHttpClient.get.mockResolvedValue({
data: userData,
});
const result = await userApi.getUser(1);
expect(result).toEqual(userData);
expect(mockHttpClient.get).toHaveBeenCalledWith(
'/users/1'
);
});
test('404エラーの適切な処理', async () => {
const error = {
response: { status: 404 },
};
mockHttpClient.get.mockRejectedValue(error);
await expect(userApi.getUser(999)).rejects.toThrow(
'ユーザーID 999 が見つかりません'
);
});
test('その他のエラーの処理', async () => {
mockHttpClient.get.mockRejectedValue(
new Error('Network Error')
);
await expect(userApi.getUser(1)).rejects.toThrow(
'ユーザー情報の取得に失敗しました'
);
});
});
Jest + Testing Library でのテスト例
React コンポーネントのテスト
javascript// src/components/UserProfile.jsx
import React, { useState, useEffect } from 'react';
export const UserProfile = ({ userId, userApi }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const userData = await userApi.getUser(userId);
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId, userApi]);
if (loading) return <div>読み込み中...</div>;
if (error) return <div role='alert'>エラー: {error}</div>;
if (!user) return <div>ユーザーが選択されていません</div>;
return (
<div>
<h2>{user.name}</h2>
<p>メール: {user.email}</p>
<p>年齢: {user.age}歳</p>
</div>
);
};
// src/components/UserProfile.test.jsx
import React from 'react';
import {
render,
screen,
waitFor,
} from '@testing-library/react';
import '@testing-library/jest-dom';
import { UserProfile } from './UserProfile';
describe('UserProfile コンポーネント', () => {
let mockUserApi;
beforeEach(() => {
mockUserApi = {
getUser: jest.fn(),
};
});
test('ユーザー情報を正常に表示', async () => {
const userData = {
name: '山田太郎',
email: 'yamada@example.com',
age: 30,
};
// Jest: APIをモック
mockUserApi.getUser.mockResolvedValue(userData);
// Testing Library: コンポーネントをレンダリング
render(
<UserProfile userId={1} userApi={mockUserApi} />
);
// Testing Library: ローディング状態を確認
expect(
screen.getByText('読み込み中...')
).toBeInTheDocument();
// Testing Library: 非同期処理を待機
await waitFor(() => {
expect(
screen.getByText('山田太郎')
).toBeInTheDocument();
});
// Testing Library: 表示内容を検証
expect(
screen.getByText('メール: yamada@example.com')
).toBeInTheDocument();
expect(
screen.getByText('年齢: 30歳')
).toBeInTheDocument();
// Jest: API呼び出しを検証
expect(mockUserApi.getUser).toHaveBeenCalledWith(1);
expect(mockUserApi.getUser).toHaveBeenCalledTimes(1);
});
test('エラー状態の表示', async () => {
const errorMessage = 'ユーザーID 999 が見つかりません';
// Jest: エラーをモック
mockUserApi.getUser.mockRejectedValue(
new Error(errorMessage)
);
render(
<UserProfile userId={999} userApi={mockUserApi} />
);
// Testing Library: エラー表示を確認
await waitFor(() => {
const errorElement = screen.getByRole('alert');
expect(errorElement).toHaveTextContent(
`エラー: ${errorMessage}`
);
});
});
test('ユーザーIDが未指定の場合', () => {
render(
<UserProfile userId={null} userApi={mockUserApi} />
);
expect(
screen.getByText('ユーザーが選択されていません')
).toBeInTheDocument();
expect(mockUserApi.getUser).not.toHaveBeenCalled();
});
});
フォームコンポーネントのテスト
javascript// src/components/ContactForm.jsx
import React, { useState } from 'react';
export const ContactForm = ({ onSubmit }) => {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = '名前は必須です';
}
if (!formData.email.trim()) {
newErrors.email = 'メールアドレスは必須です';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email =
'有効なメールアドレスを入力してください';
}
if (!formData.message.trim()) {
newErrors.message = 'メッセージは必須です';
}
return newErrors;
};
const handleSubmit = async (e) => {
e.preventDefault();
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setIsSubmitting(true);
setErrors({});
try {
await onSubmit(formData);
setFormData({ name: '', email: '', message: '' });
} catch (error) {
setErrors({ submit: '送信に失敗しました' });
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor='name'>名前 *</label>
<input
id='name'
type='text'
value={formData.name}
onChange={(e) =>
setFormData({
...formData,
name: e.target.value,
})
}
aria-invalid={errors.name ? 'true' : 'false'}
aria-describedby={
errors.name ? 'name-error' : undefined
}
/>
{errors.name && (
<div id='name-error' role='alert'>
{errors.name}
</div>
)}
</div>
<div>
<label htmlFor='email'>メールアドレス *</label>
<input
id='email'
type='email'
value={formData.email}
onChange={(e) =>
setFormData({
...formData,
email: e.target.value,
})
}
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={
errors.email ? 'email-error' : undefined
}
/>
{errors.email && (
<div id='email-error' role='alert'>
{errors.email}
</div>
)}
</div>
<div>
<label htmlFor='message'>メッセージ *</label>
<textarea
id='message'
value={formData.message}
onChange={(e) =>
setFormData({
...formData,
message: e.target.value,
})
}
aria-invalid={errors.message ? 'true' : 'false'}
aria-describedby={
errors.message ? 'message-error' : undefined
}
/>
{errors.message && (
<div id='message-error' role='alert'>
{errors.message}
</div>
)}
</div>
{errors.submit && (
<div role='alert'>{errors.submit}</div>
)}
<button type='submit' disabled={isSubmitting}>
{isSubmitting ? '送信中...' : '送信'}
</button>
</form>
);
};
// src/components/ContactForm.test.jsx
import React from 'react';
import {
render,
screen,
fireEvent,
waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { ContactForm } from './ContactForm';
describe('ContactForm コンポーネント', () => {
let mockOnSubmit;
let user;
beforeEach(() => {
mockOnSubmit = jest.fn();
user = userEvent.setup();
});
test('フォームが正常にレンダリングされる', () => {
render(<ContactForm onSubmit={mockOnSubmit} />);
expect(
screen.getByLabelText('名前 *')
).toBeInTheDocument();
expect(
screen.getByLabelText('メールアドレス *')
).toBeInTheDocument();
expect(
screen.getByLabelText('メッセージ *')
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: '送信' })
).toBeInTheDocument();
});
test('有効なデータでフォーム送信が成功する', async () => {
// Jest: onSubmitをモック
mockOnSubmit.mockResolvedValue();
render(<ContactForm onSubmit={mockOnSubmit} />);
// Testing Library: フォーム入力
await user.type(
screen.getByLabelText('名前 *'),
'山田太郎'
);
await user.type(
screen.getByLabelText('メールアドレス *'),
'yamada@example.com'
);
await user.type(
screen.getByLabelText('メッセージ *'),
'お問い合わせです'
);
const submitButton = screen.getByRole('button', {
name: '送信',
});
await user.click(submitButton);
// Jest: コールバック関数の呼び出しを検証
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
name: '山田太郎',
email: 'yamada@example.com',
message: 'お問い合わせです',
});
});
// Testing Library: フォームリセットを確認
expect(screen.getByLabelText('名前 *')).toHaveValue('');
expect(
screen.getByLabelText('メールアドレス *')
).toHaveValue('');
expect(
screen.getByLabelText('メッセージ *')
).toHaveValue('');
});
test('バリデーションエラーの表示', async () => {
render(<ContactForm onSubmit={mockOnSubmit} />);
// 空の状態で送信
const submitButton = screen.getByRole('button', {
name: '送信',
});
await user.click(submitButton);
// Testing Library: エラーメッセージの表示を確認
await waitFor(() => {
expect(
screen.getByText('名前は必須です')
).toBeInTheDocument();
expect(
screen.getByText('メールアドレスは必須です')
).toBeInTheDocument();
expect(
screen.getByText('メッセージは必須です')
).toBeInTheDocument();
});
// Jest: onSubmitが呼ばれていないことを確認
expect(mockOnSubmit).not.toHaveBeenCalled();
});
test('無効なメールアドレスのバリデーション', async () => {
render(<ContactForm onSubmit={mockOnSubmit} />);
await user.type(
screen.getByLabelText('メールアドレス *'),
'無効なメール'
);
const submitButton = screen.getByRole('button', {
name: '送信',
});
await user.click(submitButton);
await waitFor(() => {
expect(
screen.getByText(
'有効なメールアドレスを入力してください'
)
).toBeInTheDocument();
});
});
test('送信エラーの処理', async () => {
// Jest: エラーをモック
mockOnSubmit.mockRejectedValue(new Error('送信失敗'));
render(<ContactForm onSubmit={mockOnSubmit} />);
// 有効なデータを入力
await user.type(
screen.getByLabelText('名前 *'),
'山田太郎'
);
await user.type(
screen.getByLabelText('メールアドレス *'),
'yamada@example.com'
);
await user.type(
screen.getByLabelText('メッセージ *'),
'テスト'
);
const submitButton = screen.getByRole('button', {
name: '送信',
});
await user.click(submitButton);
// Testing Library: エラーメッセージの表示を確認
await waitFor(() => {
expect(
screen.getByText('送信に失敗しました')
).toBeInTheDocument();
});
// 送信ボタンが元に戻ることを確認
expect(
screen.getByRole('button', { name: '送信' })
).toBeInTheDocument();
});
test('送信中状態の表示', async () => {
// Jest: 送信処理を遅延させる
mockOnSubmit.mockImplementation(
() =>
new Promise((resolve) => setTimeout(resolve, 100))
);
render(<ContactForm onSubmit={mockOnSubmit} />);
// フォーム入力
await user.type(
screen.getByLabelText('名前 *'),
'山田太郎'
);
await user.type(
screen.getByLabelText('メールアドレス *'),
'yamada@example.com'
);
await user.type(
screen.getByLabelText('メッセージ *'),
'テスト'
);
const submitButton = screen.getByRole('button', {
name: '送信',
});
await user.click(submitButton);
// Testing Library: 送信中状態を確認
expect(
screen.getByRole('button', { name: '送信中...' })
).toBeDisabled();
// 送信完了を待機
await waitFor(() => {
expect(
screen.getByRole('button', { name: '送信' })
).not.toBeDisabled();
});
});
});
どちらを選ぶべきか:判断基準
プロジェクトの性質による使い分け
ライブラリ・ユーティリティ関数の開発
javascript// パッケージ開発時:Jest単体が最適
// src/utils/dateUtils.js
export const formatDate = (date, format = 'YYYY-MM-DD') => {
// 日付フォーマット処理
};
export const addDays = (date, days) => {
// 日付加算処理
};
// Jest単体でのテスト
describe('dateUtils', () => {
test('formatDate - デフォルトフォーマット', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('2024-01-15');
});
test('addDays - 正常な日付加算', () => {
const date = new Date('2024-01-15');
const result = addDays(date, 7);
expect(result.getDate()).toBe(22);
});
});
React アプリケーション開発
javascript// React開発時:Jest + Testing Library の組み合わせが最適
test('検索フォームの動作', async () => {
const mockOnSearch = jest.fn(); // Jest のモック機能
render(<SearchForm onSearch={mockOnSearch} />); // Testing Library
const input =
screen.getByPlaceholderText('検索キーワード'); // Testing Library
const button = screen.getByRole('button', {
name: '検索',
}); // Testing Library
await userEvent.type(input, 'React'); // Testing Library
await userEvent.click(button); // Testing Library
expect(mockOnSearch).toHaveBeenCalledWith('React'); // Jest のアサーション
});
チーム構成による選択
経験豊富な開発チーム
javascript// 複雑なテストシナリオに対応可能
describe('高度なテストシナリオ', () => {
test('複数のモックとタイマーを組み合わせ', async () => {
// Jest の高度な機能を活用
jest.useFakeTimers();
const mockApi = jest.fn().mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(() => resolve({ data: 'test' }), 1000);
})
);
render(<Component api={mockApi} />);
// タイマーを進める
jest.advanceTimersByTime(1000);
await waitFor(() => {
expect(screen.getByText('test')).toBeInTheDocument();
});
jest.useRealTimers();
});
});
学習コストを重視するチーム
javascript// シンプルなテストから始める
test('基本的な表示テスト', () => {
render(<WelcomeMessage name='太郎' />);
// 直感的でわかりやすいテスト
expect(
screen.getByText('こんにちは、太郎さん!')
).toBeInTheDocument();
});
test('ボタンクリックテスト', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByText('カウントアップ');
await user.click(button);
expect(
screen.getByText('カウント: 1')
).toBeInTheDocument();
});
段階的導入戦略
フェーズ 1:Jest 単体で基礎固め
javascript// 最初はビジネスロジックのテストから
describe('商品価格計算', () => {
test('消費税込み価格計算', () => {
const calculateTaxIncluded = (price, taxRate = 0.1) => {
return Math.floor(price * (1 + taxRate));
};
expect(calculateTaxIncluded(1000)).toBe(1100);
expect(calculateTaxIncluded(999)).toBe(1098);
});
});
フェーズ 2:Testing Library 追加
javascript// UIコンポーネントテストの導入
test('価格表示コンポーネント', () => {
render(<PriceDisplay price={1000} taxRate={0.1} />);
expect(
screen.getByText('本体価格: ¥1,000')
).toBeInTheDocument();
expect(
screen.getByText('税込価格: ¥1,100')
).toBeInTheDocument();
});
フェーズ 3:統合テストの追加
javascript// 複雑なユーザーフローのテスト
test('商品購入フロー', async () => {
const user = userEvent.setup();
render(<ShoppingCart />);
// 商品追加
await user.click(screen.getByText('カートに追加'));
expect(
screen.getByText('カート (1)')
).toBeInTheDocument();
// 決済へ進む
await user.click(screen.getByText('レジに進む'));
expect(
screen.getByText('お支払い方法を選択')
).toBeInTheDocument();
});
よくある落とし穴と対策
設定不備によるエラー
javascriptError: Cannot find module '@testing-library/jest-dom'
at Object.<anonymous> (src/setupTests.js:1:1)
SyntaxError: Cannot use import statement outside a module
at Object.<anonymous> (src/components/App.test.jsx:1:1)
解決策:適切な設定
javascript// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
moduleNameMapping: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
};
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-react', { runtime: 'automatic' }],
],
};
Testing Library 使用時の典型的エラー
javascriptTestingLibraryElementError: Unable to find an accessible element with the role "button" and name "送信"
Found the following similar elements:
<button type="submit">Submit</button>
解決策:適切なクエリの使用
javascript// 問題のあるテスト
const button = screen.getByRole('button', { name: '送信' });
// 修正版
const button = screen.getByRole('button', {
name: 'Submit',
});
// または
const button = screen.getByRole('button', {
name: /submit/i,
});
まとめ
Jest と Testing Library は、それぞれ異なる役割を持つ補完的なツールです。Jest はテスト実行基盤として、Testing Library はテスト記述支援として機能し、多くの場合、両者を組み合わせて使用することが最適解となります。
適切な選択指針
プロジェクトタイプ | 推奨構成 | 理由 |
---|---|---|
ライブラリ・ユーティリティ開発 | Jest 単体 | DOM 操作が不要、軽量構成 |
React/Vue アプリケーション | Jest + Testing Library | UI テストに最適化 |
Node.js API 開発 | Jest 単体 | サーバーサイドロジックに集中 |
フルスタック開発 | Jest + Testing Library | フロントエンド・バックエンド両対応 |
現代のフロントエンド開発において、テストは品質保証の要です。Jest と Testing Library それぞれの特徴を理解し、プロジェクトの要件に応じて適切に選択・組み合わせることで、保守性が高く信頼できるアプリケーションを構築できるでしょう。
まずは小さなプロジェクトから始めて、段階的にテストカバレッジを向上させていくことをお勧めします。テストが開発の重要なパートナーとなることで、より安心してコードを書けるようになるはずです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来