# Jest を使った JavaScript テストの書き方
Jest は Facebook が開発した JavaScript のテストフレームワークです。設定が簡単で、モック機能やカバレッジレポートなど、テストに必要な機能が一通り揃っています。
# Jest のインストール
プロジェクトに Jest をインストールします。
npm install --save-dev jest
TypeScript を使う場合は、追加で以下もインストールします。
npm install --save-dev @types/jest ts-jest
# package.json の設定
package.json にテストスクリプトを追加します。
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
# 基本的なテストの書き方
# テストファイルの作成
テストファイルは *.test.js または *.spec.js という命名規則で作成します。Jest は自動的にこれらのファイルを検出します。
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
// math.test.js
const { add, subtract } = require('./math');
describe('Math functions', () => {
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts 5 - 3 to equal 2', () => {
expect(subtract(5, 3)).toBe(2);
});
});
# テストの実行
npm test
# マッチャー(Matcher)
Jest では expect() とマッチャーを使って値を検証します。
# 基本的なマッチャー
test('基本的なマッチャーの例', () => {
// 等価性
expect(2 + 2).toBe(4);
expect({ name: 'John' }).toEqual({ name: 'John' });
// 真偽値
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// 数値
expect(2 + 2).toBeGreaterThan(3);
expect(2 + 2).toBeGreaterThanOrEqual(4);
expect(2 + 2).toBeLessThan(5);
expect(2 + 2).toBeLessThanOrEqual(4);
// 文字列
expect('team').toMatch(/ea/);
expect('team').toContain('ea');
// 配列
expect(['apple', 'banana', 'orange']).toContain('banana');
expect([1, 2, 3]).toHaveLength(3);
// オブジェクト
expect({ name: 'John', age: 30 }).toHaveProperty('name');
expect({ name: 'John', age: 30 }).toHaveProperty('age', 30);
});
# 否定のマッチャー
.not を使って否定のテストを書けます。
test('否定のマッチャー', () => {
expect(2 + 2).not.toBe(5);
expect('hello').not.toContain('world');
});
# 非同期処理のテスト
# Promise のテスト
// fetchUser.js
async function fetchUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
}
module.exports = { fetchUser };
// fetchUser.test.js
const { fetchUser } = require('./fetchUser');
test('ユーザー情報を取得する', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
});
test('存在しないユーザーでエラーが発生する', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
# コールバックのテスト
// callback.js
function fetchData(callback) {
setTimeout(() => {
callback(null, { data: 'test' });
}, 100);
}
module.exports = { fetchData };
// callback.test.js
const { fetchData } = require('./callback');
test('コールバックでデータを取得する', (done) => {
fetchData((error, data) => {
expect(error).toBeNull();
expect(data).toEqual({ data: 'test' });
done();
});
});
# モック(Mock)
モックを使うと、外部依存を模倣してテストを独立させられます。
# 関数のモック
// utils.js
function getRandomNumber() {
return Math.random();
}
module.exports = { getRandomNumber };
// utils.test.js
const { getRandomNumber } = require('./utils');
test('ランダムな数値を返す', () => {
// Math.random をモック化
const mockMath = Object.create(global.Math);
mockMath.random = jest.fn(() => 0.5);
global.Math = mockMath;
expect(getRandomNumber()).toBe(0.5);
});
# モジュールのモック
// api.js
const axios = require('axios');
async function getUserData(userId) {
const response = await axios.get(`/users/${userId}`);
return response.data;
}
module.exports = { getUserData };
// api.test.js
const axios = require('axios');
const { getUserData } = require('./api');
jest.mock('axios');
test('ユーザーデータを取得する', async () => {
const mockUser = { id: 1, name: 'John' };
axios.get.mockResolvedValue({ data: mockUser });
const user = await getUserData(1);
expect(user).toEqual(mockUser);
expect(axios.get).toHaveBeenCalledWith('/users/1');
});
# モック関数の作成
test('モック関数の使用例', () => {
const mockFn = jest.fn();
mockFn('arg1', 'arg2');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(1);
// 戻り値を設定
const mockFnWithReturn = jest.fn(() => 'return value');
expect(mockFnWithReturn()).toBe('return value');
// 複数の呼び出しで異なる値を返す
const mockFnMultiple = jest
.fn()
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('default');
expect(mockFnMultiple()).toBe('first');
expect(mockFnMultiple()).toBe('second');
expect(mockFnMultiple()).toBe('default');
expect(mockFnMultiple()).toBe('default');
});
# セットアップとティアダウン
テストの前後で実行する処理を設定できます。
// 各テストの前に実行
beforeEach(() => {
// テストデータの初期化など
});
// 各テストの後に実行
afterEach(() => {
// クリーンアップ処理など
});
// すべてのテストの前に一度だけ実行
beforeAll(() => {
// データベース接続など
});
// すべてのテストの後に一度だけ実行
afterAll(() => {
// データベース切断など
});
# 使用例
describe('データベース操作のテスト', () => {
let db;
beforeAll(async () => {
db = await connectToDatabase();
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.clear();
});
test('ユーザーを追加する', async () => {
const user = await db.addUser({ name: 'John' });
expect(user).toHaveProperty('id');
});
test('ユーザーを取得する', async () => {
await db.addUser({ name: 'John' });
const users = await db.getUsers();
expect(users).toHaveLength(1);
});
});
# テストのグループ化
describe ブロックでテストをグループ化できます。
describe('Calculator', () => {
describe('加算', () => {
test('正の数を足す', () => {
expect(add(2, 3)).toBe(5);
});
test('負の数を足す', () => {
expect(add(-2, -3)).toBe(-5);
});
});
describe('減算', () => {
test('正の数を引く', () => {
expect(subtract(5, 3)).toBe(2);
});
test('負の数を引く', () => {
expect(subtract(-5, -3)).toBe(-2);
});
});
});
# スナップショットテスト
UI コンポーネントや設定ファイルなどの出力が期待通りかを確認するのに便利です。
// component.js
function createButton(text, className) {
return `<button class="${className}">${text}</button>`;
}
module.exports = { createButton };
// component.test.js
const { createButton } = require('./component');
test('ボタンコンポーネントのスナップショット', () => {
const button = createButton('Click me', 'btn-primary');
expect(button).toMatchSnapshot();
});
初回実行時にスナップショットが作成され、以降のテストで比較されます。スナップショットを更新する場合は npm test -- -u を実行します。
# エラーのテスト
// error.js
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
module.exports = { divide };
// error.test.js
const { divide } = require('./error');
test('ゼロで割るとエラーが発生する', () => {
expect(() => divide(10, 0)).toThrow();
expect(() => divide(10, 0)).toThrow('Division by zero');
expect(() => divide(10, 0)).toThrow(/Division/);
});
# タイマーのモック
setTimeout や setInterval を使う関数をテストする場合、タイマーをモック化できます。
// timer.js
function delayedMessage(callback, delay) {
setTimeout(() => {
callback('Hello');
}, delay);
}
module.exports = { delayedMessage };
// timer.test.js
const { delayedMessage } = require('./timer');
jest.useFakeTimers();
test('遅延メッセージを送信する', () => {
const callback = jest.fn();
delayedMessage(callback, 1000);
// タイマーがまだ実行されていないことを確認
expect(callback).not.toHaveBeenCalled();
// タイマーを進める
jest.advanceTimersByTime(1000);
// コールバックが呼ばれたことを確認
expect(callback).toHaveBeenCalledWith('Hello');
});
# カバレッジレポート
テストカバレッジを確認できます。
npm run test:coverage
カバレッジレポートが coverage ディレクトリに生成されます。
# よくある設定(jest.config.js)
// jest.config.js
module.exports = {
// テスト環境
testEnvironment: 'node', // 'jsdom' でブラウザ環境をシミュレート
// テストファイルのパターン
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
// モジュールの解決
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx'],
// カバレッジの設定
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/**/*.test.{js,jsx}',
'!src/index.js',
],
// カバレッジの閾値
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
// セットアップファイル
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
# ベストプラクティス
- テストは独立させる: テスト同士が依存しないようにする
- 明確なテスト名: テストが何を検証しているか分かる名前にする
- AAA パターン: Arrange(準備)、Act(実行)、Assert(検証)の順で書く
- モックは最小限に: 必要な場合のみモックを使う
- エッジケースをテスト: 正常系だけでなく異常系もテストする
test('ユーザー登録のテスト', () => {
// Arrange: テストデータの準備
const userData = {
name: 'John Doe',
email: 'john@example.com',
};
// Act: テスト対象の実行
const result = registerUser(userData);
// Assert: 結果の検証
expect(result).toHaveProperty('id');
expect(result.name).toBe('John Doe');
});