Skip to content

Jestセットアップとベストプラクティス完全ガイド

🧪 Jestセットアップとベストプラクティス完全ガイド

Section titled “🧪 Jestセットアップとベストプラクティス完全ガイド”

Jestの実務で使えるセットアップ方法とベストプラクティスを、詳細な実装例とともに解説します。

🏗️ 1. プロジェクトセットアップ

Section titled “🏗️ 1. プロジェクトセットアップ”
Terminal window
# プロジェクトの初期化
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
Terminal window
# 既存のプロジェクトにJestを追加
npm install --save-dev jest
# 設定ファイルの生成
npx jest --init
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.json

基本的な設定ファイル(JavaScript)

Section titled “基本的な設定ファイル(JavaScript)”
jest.config.js
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'],
};
jest.config.js
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__/**',
],
};
jest.setup.js
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';
Terminal window
# Create React Appは既にJestが設定済み
npx create-react-app my-app
cd my-app
npm test
Terminal window
# 依存関係のインストール
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-jest
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-react', { runtime: 'automatic' }],
],
};
jest.config.js
module.exports = {
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
};
Terminal window
# 依存関係のインストール
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
jest.config.js
const 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);
Terminal window
# 依存関係のインストール
npm install --save-dev jest supertest
jest.config.js
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.js'],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/__tests__/**',
],
};
__tests__/api.test.js
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);
});
});
// 良い例: 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);
});
});
utils/helpers.js
export const fetchUser = async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
};
// __tests__/helpers.test.js
import { 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');
});
});
__mocks__/axios.js
module.exports = {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} })),
};
// __tests__/api.test.js
jest.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);
});
});
__tests__/test-data.js
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',
},
};
__tests__/factories.js
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 });
jest.setup.js
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);
});
// 良い例: async/awaitを使用
test('非同期処理のテスト', async () => {
const result = await fetchData();
expect(result).toBeDefined();
});
// 悪い例: コールバックやPromiseチェーン
test('非同期処理のテスト(悪い例)', (done) => {
fetchData().then((result) => {
expect(result).toBeDefined();
done();
});
});
test('エラーが発生する場合', async () => {
await expect(asyncFunction()).rejects.toThrow('Error message');
});
test('エラーが発生しない場合', async () => {
await expect(asyncFunction()).resolves.toBeDefined();
});

スナップショットテストの使用

Section titled “スナップショットテストの使用”
// 良い例: スナップショットテスト
test('コンポーネントのスナップショット', () => {
const { container } = render(<Button>Click me</Button>);
expect(container).toMatchSnapshot();
});
// スナップショットの更新
// npm test -- -u

スナップショットテストの注意点

Section titled “スナップショットテストの注意点”
// スナップショットテストは慎重に使用
// - 頻繁に変更されるコンポーネントには不向き
// - 重要なUI要素の回帰テストに適している
// - スナップショットの更新を定期的にレビュー
jest.config.js
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,
},
},
};
Terminal window
# カバレッジレポートを生成
npm test -- --coverage
# カバレッジレポートをHTMLで表示
npm test -- --coverage --coverageReporters=html
jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
'./src/utils/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
};
.github/workflows/jest.yml
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-umbrella
.gitlab-ci.yml
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.xml

6. デバッグとトラブルシューティング

Section titled “6. デバッグとトラブルシューティング”
Terminal window
# デバッグモードでテストを実行
node --inspect-brk node_modules/.bin/jest --runInBand
# 特定のテストをデバッグ
npm test -- --testNamePattern="test name"
// テスト中のログを確認
test('デバッグ例', () => {
console.log('Debug information');
// テストコード
});

問題1: モジュールが見つからない

Section titled “問題1: モジュールが見つからない”

原因:

  • パスの解決が正しくない
  • moduleNameMapperの設定が不適切

解決策:

jest.config.js
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
};

原因:

  • 非同期処理の待機時間が不足
  • テストの実行時間が長い

解決策:

// タイムアウトを延長
test('遅いテスト', async () => {
// テストコード
}, 10000); // 10秒
// または、設定ファイルでグローバルに設定
// jest.config.js
module.exports = {
testTimeout: 10000, // 10秒
};

原因:

  • モックのタイミングが不適切
  • モジュールのキャッシュ

解決策:

// モックをテストの前に配置
jest.mock('./module');
// または、モックをリセット
beforeEach(() => {
jest.clearAllMocks();
});

7. 実務でのベストプラクティス

Section titled “7. 実務でのベストプラクティス”
// 良い例: 明確で説明的な名前
describe('Button Component', () => {
test('クリック時にonClickハンドラーが呼ばれる', () => {
// ...
});
test('disabledプロパティがtrueの場合、クリックできない', () => {
// ...
});
});
// 悪い例: 曖昧な名前
describe('Button', () => {
test('test1', () => {
// ...
});
});
// 良い例: 各テストが独立している
describe('User Service', () => {
beforeEach(() => {
// 各テストの前にクリーンアップ
jest.clearAllMocks();
});
test('ユーザーを作成する', () => {
// テストコード
});
test('ユーザーを取得する', () => {
// テストコード(前のテストに依存しない)
});
});
// 定数とヘルパー関数の使用
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();
});
// パフォーマンステストの例
test('大量のデータを処理できる', () => {
const startTime = performance.now();
processLargeData(10000);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(1000); // 1秒以内
});

これで、Jestのセットアップとベストプラクティスを実務で活用できるようになりました。