Jestセットアップとベストプラクティス完全ガイド
🧪 Jestセットアップとベストプラクティス完全ガイド
Section titled “🧪 Jestセットアップとベストプラクティス完全ガイド”Jestの実務で使えるセットアップ方法とベストプラクティスを、詳細な実装例とともに解説します。
🏗️ 1. プロジェクトセットアップ
Section titled “🏗️ 1. プロジェクトセットアップ”1.1 初期セットアップ
Section titled “1.1 初期セットアップ”新規プロジェクトの場合
Section titled “新規プロジェクトの場合”# プロジェクトの初期化npm init -y
# Jestのインストールnpm install --save-dev jest
# TypeScriptを使用する場合npm install --save-dev typescript @types/jest ts-jest
# Reactを使用する場合npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event既存プロジェクトへの追加
Section titled “既存プロジェクトへの追加”# 既存のプロジェクトにJestを追加npm install --save-dev jest
# 設定ファイルの生成npx jest --init1.2 プロジェクト構造
Section titled “1.2 プロジェクト構造”推奨されるディレクトリ構造
Section titled “推奨されるディレクトリ構造”project-root/├── src/│ ├── components/│ │ ├── Button.tsx│ │ └── Button.test.tsx│ ├── utils/│ │ ├── helpers.ts│ │ └── helpers.test.ts│ └── __tests__/│ └── integration/│ └── app.test.tsx├── __mocks__/│ └── axios.ts├── jest.config.js├── jest.setup.js└── package.json1.3 設定ファイルの作成
Section titled “1.3 設定ファイルの作成”基本的な設定ファイル(JavaScript)
Section titled “基本的な設定ファイル(JavaScript)”module.exports = { // テスト環境 testEnvironment: 'jest-environment-jsdom',
// テストファイルの検索パターン testMatch: [ '**/__tests__/**/*.(js|jsx|ts|tsx)', '**/*.(test|spec).(js|jsx|ts|tsx)', ],
// モジュールの解決 moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', '\\.(css|less|scss|sass)$': 'identity-obj-proxy', },
// セットアップファイル setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
// カバレッジの設定 collectCoverageFrom: [ 'src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/**/*.stories.{js,jsx,ts,tsx}', '!src/**/__tests__/**', ],
// カバレッジの閾値 coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, },
// トランスフォームの設定 transform: { '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', },
// モジュールの無視パターン transformIgnorePatterns: [ '/node_modules/(?!(module-to-transform)/)', ],
// モジュールの拡張子 moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],};TypeScriptプロジェクトの場合
Section titled “TypeScriptプロジェクトの場合”module.exports = { preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom', roots: ['<rootDir>/src'], testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], transform: { '^.+\\.(ts|tsx)$': 'ts-jest', }, moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/__tests__/**', ],};セットアップファイル
Section titled “セットアップファイル”import '@testing-library/jest-dom';
// グローバルモックの設定global.console = { ...console, // テスト中にconsole.logを無効化(必要に応じて) // log: jest.fn(), // error: jest.fn(), // warn: jest.fn(),};
// タイマーのモックjest.useFakeTimers();
// 環境変数の設定process.env.NODE_ENV = 'test';2. 環境別セットアップ
Section titled “2. 環境別セットアップ”2.1 Reactプロジェクト
Section titled “2.1 Reactプロジェクト”Create React Appの場合
Section titled “Create React Appの場合”# Create React Appは既にJestが設定済みnpx create-react-app my-appcd my-appnpm testVite + Reactの場合
Section titled “Vite + Reactの場合”# 依存関係のインストールnpm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom
# Babelの設定npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-jestmodule.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-react', { runtime: 'automatic' }], ],};module.exports = { testEnvironment: 'jest-environment-jsdom', setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, transform: { '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', },};2.2 Next.jsプロジェクト
Section titled “2.2 Next.jsプロジェクト”Next.js 13+ (App Router)
Section titled “Next.js 13+ (App Router)”# 依存関係のインストールnpm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-domconst nextJest = require('next/jest');
const createJestConfig = nextJest({ dir: './',});
const customJestConfig = { setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], testEnvironment: 'jest-environment-jsdom', moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, collectCoverageFrom: [ 'src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/**/__tests__/**', ],};
module.exports = createJestConfig(customJestConfig);2.3 Node.jsプロジェクト
Section titled “2.3 Node.jsプロジェクト”バックエンドAPIのテスト
Section titled “バックエンドAPIのテスト”# 依存関係のインストールnpm install --save-dev jest supertestmodule.exports = { testEnvironment: 'node', testMatch: ['**/__tests__/**/*.test.js'], collectCoverageFrom: [ 'src/**/*.js', '!src/**/__tests__/**', ],};const request = require('supertest');const app = require('../src/app');
describe('API Tests', () => { test('GET /api/users', async () => { const response = await request(app) .get('/api/users') .expect(200);
expect(response.body).toHaveLength(0); });});3. ベストプラクティス
Section titled “3. ベストプラクティス”3.1 テストの構造化
Section titled “3.1 テストの構造化”AAAパターン(Arrange-Act-Assert)
Section titled “AAAパターン(Arrange-Act-Assert)”// 良い例: AAAパターンを使用describe('Button Component', () => { test('クリック時にonClickが呼ばれる', () => { // Arrange: テストの準備 const handleClick = jest.fn(); const { getByRole } = render(<Button onClick={handleClick}>Click me</Button>); const button = getByRole('button');
// Act: テストの実行 fireEvent.click(button);
// Assert: 結果の検証 expect(handleClick).toHaveBeenCalledTimes(1); });});3.2 モックの活用
Section titled “3.2 モックの活用”関数のモック
Section titled “関数のモック”export const fetchUser = async (userId) => { const response = await fetch(`/api/users/${userId}`); return response.json();};
// __tests__/helpers.test.jsimport { fetchUser } from '../utils/helpers';
// fetchをモックglobal.fetch = jest.fn();
describe('fetchUser', () => { test('ユーザー情報を取得する', async () => { // Arrange const mockUser = { id: 1, name: 'John' }; global.fetch.mockResolvedValueOnce({ json: async () => mockUser, });
// Act const user = await fetchUser(1);
// Assert expect(user).toEqual(mockUser); expect(global.fetch).toHaveBeenCalledWith('/api/users/1'); });});モジュールのモック
Section titled “モジュールのモック”module.exports = { get: jest.fn(() => Promise.resolve({ data: {} })), post: jest.fn(() => Promise.resolve({ data: {} })),};
// __tests__/api.test.jsjest.mock('axios');const axios = require('axios');
describe('API Tests', () => { test('ユーザー情報を取得する', async () => { const mockUser = { id: 1, name: 'John' }; axios.get.mockResolvedValue({ data: mockUser });
const user = await fetchUser(1);
expect(user).toEqual(mockUser); });});3.3 テストデータの管理
Section titled “3.3 テストデータの管理”テストデータの分離
Section titled “テストデータの分離”export const testUsers = { admin: { id: 1, name: 'Admin User', email: 'admin@example.com', role: 'admin', }, user: { id: 2, name: 'Regular User', email: 'user@example.com', role: 'user', },};
export const testProducts = { laptop: { id: 1, name: 'Laptop', price: 999.99, category: 'Electronics', }, phone: { id: 2, name: 'Phone', price: 699.99, category: 'Electronics', },};ファクトリー関数の使用
Section titled “ファクトリー関数の使用”export const createUser = (overrides = {}) => ({ id: 1, name: 'Test User', email: 'test@example.com', role: 'user', ...overrides,});
export const createProduct = (overrides = {}) => ({ id: 1, name: 'Test Product', price: 99.99, category: 'Test', ...overrides,});
// 使用例const adminUser = createUser({ role: 'admin' });const expensiveProduct = createProduct({ price: 999.99 });3.4 カスタムマッチャーの作成
Section titled “3.4 カスタムマッチャーの作成”カスタムマッチャーの定義
Section titled “カスタムマッチャーの定義”expect.extend({ toBeWithinRange(received, floor, ceiling) { const pass = received >= floor && received <= ceiling; if (pass) { return { message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`, pass: true, }; } else { return { message: () => `expected ${received} to be within range ${floor} - ${ceiling}`, pass: false, }; } },});
// 使用例test('数値が範囲内にある', () => { expect(100).toBeWithinRange(90, 110);});3.5 非同期処理のテスト
Section titled “3.5 非同期処理のテスト”async/awaitの使用
Section titled “async/awaitの使用”// 良い例: async/awaitを使用test('非同期処理のテスト', async () => { const result = await fetchData(); expect(result).toBeDefined();});
// 悪い例: コールバックやPromiseチェーンtest('非同期処理のテスト(悪い例)', (done) => { fetchData().then((result) => { expect(result).toBeDefined(); done(); });});エラーハンドリングのテスト
Section titled “エラーハンドリングのテスト”test('エラーが発生する場合', async () => { await expect(asyncFunction()).rejects.toThrow('Error message');});
test('エラーが発生しない場合', async () => { await expect(asyncFunction()).resolves.toBeDefined();});3.6 スナップショットテスト
Section titled “3.6 スナップショットテスト”スナップショットテストの使用
Section titled “スナップショットテストの使用”// 良い例: スナップショットテストtest('コンポーネントのスナップショット', () => { const { container } = render(<Button>Click me</Button>); expect(container).toMatchSnapshot();});
// スナップショットの更新// npm test -- -uスナップショットテストの注意点
Section titled “スナップショットテストの注意点”// スナップショットテストは慎重に使用// - 頻繁に変更されるコンポーネントには不向き// - 重要なUI要素の回帰テストに適している// - スナップショットの更新を定期的にレビュー4. カバレッジとレポート
Section titled “4. カバレッジとレポート”4.1 カバレッジの設定
Section titled “4.1 カバレッジの設定”カバレッジレポートの生成
Section titled “カバレッジレポートの生成”module.exports = { collectCoverage: true, collectCoverageFrom: [ 'src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/**/__tests__/**', '!src/**/*.stories.{js,jsx,ts,tsx}', ], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, },};カバレッジレポートの実行
Section titled “カバレッジレポートの実行”# カバレッジレポートを生成npm test -- --coverage
# カバレッジレポートをHTMLで表示npm test -- --coverage --coverageReporters=html4.2 カバレッジの閾値
Section titled “4.2 カバレッジの閾値”プロジェクト全体の閾値
Section titled “プロジェクト全体の閾値”module.exports = { coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, },};ファイル単位の閾値
Section titled “ファイル単位の閾値”module.exports = { coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, './src/utils/': { branches: 90, functions: 90, lines: 90, statements: 90, }, },};5. CI/CD統合
Section titled “5. CI/CD統合”5.1 GitHub Actions
Section titled “5.1 GitHub Actions”GitHub Actionsの設定
Section titled “GitHub Actionsの設定”name: Jest Tests
on: push: branches: [main, develop] pull_request: branches: [main, develop]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
- uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm'
- name: Install dependencies run: npm ci
- name: Run tests run: npm test -- --coverage
- name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info flags: unittests name: codecov-umbrella5.2 GitLab CI/CD
Section titled “5.2 GitLab CI/CD”GitLab CI/CDの設定
Section titled “GitLab CI/CDの設定”stages: - test
jest: stage: test image: node:18 before_script: - npm ci script: - npm test -- --coverage coverage: '/Lines\s*:\s*(\d+\.\d+)%/' artifacts: reports: coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml6. デバッグとトラブルシューティング
Section titled “6. デバッグとトラブルシューティング”6.1 デバッグ方法
Section titled “6.1 デバッグ方法”デバッグモードの実行
Section titled “デバッグモードの実行”# デバッグモードでテストを実行node --inspect-brk node_modules/.bin/jest --runInBand
# 特定のテストをデバッグnpm test -- --testNamePattern="test name"コンソールログの確認
Section titled “コンソールログの確認”// テスト中のログを確認test('デバッグ例', () => { console.log('Debug information'); // テストコード});6.2 よくある問題と解決策
Section titled “6.2 よくある問題と解決策”問題1: モジュールが見つからない
Section titled “問題1: モジュールが見つからない”原因:
- パスの解決が正しくない
- moduleNameMapperの設定が不適切
解決策:
module.exports = { moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', '\\.(css|less|scss|sass)$': 'identity-obj-proxy', },};問題2: タイムアウトエラー
Section titled “問題2: タイムアウトエラー”原因:
- 非同期処理の待機時間が不足
- テストの実行時間が長い
解決策:
// タイムアウトを延長test('遅いテスト', async () => { // テストコード}, 10000); // 10秒
// または、設定ファイルでグローバルに設定// jest.config.jsmodule.exports = { testTimeout: 10000, // 10秒};問題3: モックが機能しない
Section titled “問題3: モックが機能しない”原因:
- モックのタイミングが不適切
- モジュールのキャッシュ
解決策:
// モックをテストの前に配置jest.mock('./module');
// または、モックをリセットbeforeEach(() => { jest.clearAllMocks();});7. 実務でのベストプラクティス
Section titled “7. 実務でのベストプラクティス”7.1 テストの命名規則
Section titled “7.1 テストの命名規則”// 良い例: 明確で説明的な名前describe('Button Component', () => { test('クリック時にonClickハンドラーが呼ばれる', () => { // ... });
test('disabledプロパティがtrueの場合、クリックできない', () => { // ... });});
// 悪い例: 曖昧な名前describe('Button', () => { test('test1', () => { // ... });});7.2 テストの独立性
Section titled “7.2 テストの独立性”// 良い例: 各テストが独立しているdescribe('User Service', () => { beforeEach(() => { // 各テストの前にクリーンアップ jest.clearAllMocks(); });
test('ユーザーを作成する', () => { // テストコード });
test('ユーザーを取得する', () => { // テストコード(前のテストに依存しない) });});7.3 テストのメンテナンス性
Section titled “7.3 テストのメンテナンス性”// 定数とヘルパー関数の使用const TEST_DATA = { user: { id: 1, name: 'Test User', email: 'test@example.com', },};
const renderComponent = (props = {}) => { return render(<Component {...TEST_DATA.user} {...props} />);};
test('コンポーネントのテスト', () => { const { getByText } = renderComponent(); expect(getByText('Test User')).toBeInTheDocument();});7.4 パフォーマンステスト
Section titled “7.4 パフォーマンステスト”// パフォーマンステストの例test('大量のデータを処理できる', () => { const startTime = performance.now(); processLargeData(10000); const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(1000); // 1秒以内});これで、Jestのセットアップとベストプラクティスを実務で活用できるようになりました。