jest の使い方完全ガイド【v30対応】JavaScriptテストフレームワークの決定版
一言でいうと
Jest は、Meta(旧Facebook)が開発したJavaScript/TypeScript向けのオールインワンテストフレームワークです。テストランナー、アサーション、モック、カバレッジ計測がすべて内蔵されており、設定なし(Zero Config)ですぐにテストを始められます。
どんな時に使う?
- React/Node.jsアプリケーションのユニットテスト・統合テスト — コンポーネントやAPIハンドラのテストを、追加ライブラリなしで記述したい場合
- スナップショットテスト — UIコンポーネントやAPIレスポンスの出力が意図せず変更されていないかを自動検知したい場合
- モック・スタブを多用するテスト — 外部API呼び出しやDB接続などの依存を簡単に差し替えて、隔離されたテストを書きたい場合
インストール
# npm
npm install --save-dev jest
# yarn
yarn add --dev jest
# pnpm
pnpm add --save-dev jest
TypeScriptで使う場合
npm install --save-dev jest ts-jest @jest/globals @types/jest
jest.config.ts を作成します:
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
};
export default config;
package.json にスクリプトを追加します:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
基本的な使い方
最もシンプルなテストの例です。
src/sum.ts
export function sum(a: number, b: number): number {
return a + b;
}
src/__tests__/sum.test.ts
import { sum } from '../sum';
describe('sum', () => {
it('1 + 2 は 3 を返す', () => {
expect(sum(1, 2)).toBe(3);
});
it('負の数同士の加算が正しく動作する', () => {
expect(sum(-1, -2)).toBe(-3);
});
it('0を加算しても値が変わらない', () => {
expect(sum(5, 0)).toBe(5);
});
});
実行:
npm test
出力例:
PASS src/__tests__/sum.test.ts
sum
✓ 1 + 2 は 3 を返す (2 ms)
✓ 負の数同士の加算が正しく動作する
✓ 0を加算しても値が変わらない (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
よく使うAPI
1. マッチャー(Matchers)
Jestのアサーションの中核です。expect() と組み合わせて使います。
describe('マッチャーの例', () => {
// 厳密等価
it('toBe — プリミティブ値の比較', () => {
expect(1 + 1).toBe(2);
expect('hello').toBe('hello');
expect(true).toBe(true);
});
// ディープイコール(オブジェクト・配列の比較)
it('toEqual — オブジェクトの深い比較', () => {
const user = { name: 'Taro', age: 30 };
expect(user).toEqual({ name: 'Taro', age: 30 });
});
// 部分一致
it('toMatchObject — オブジェクトの部分一致', () => {
const user = { name: 'Taro', age: 30, email: 'taro@example.com' };
expect(user).toMatchObject({ name: 'Taro', age: 30 });
});
// 真偽値系
it('Truthiness マッチャー', () => {
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('value').toBeDefined();
expect(1).toBeTruthy();
expect(0).toBeFalsy();
});
// 数値比較
it('数値マッチャー', () => {
expect(10).toBeGreaterThan(9);
expect(10).toBeGreaterThanOrEqual(10);
expect(10).toBeLessThan(11);
expect(0.1 + 0.2).toBeCloseTo(0.3); // 浮動小数点対策
});
// 文字列・配列
it('文字列と配列のマッチャー', () => {
expect('Hello World').toMatch(/World/);
expect(['apple', 'banana', 'cherry']).toContain('banana');
expect([1, 2, 3]).toHaveLength(3);
});
// 例外
it('toThrow — 例外の検証', () => {
const throwError = () => {
throw new Error('Something went wrong');
};
expect(throwError).toThrow('Something went wrong');
expect(throwError).toThrow(Error);
});
});
2. モック関数(jest.fn() / jest.mock())
外部依存を差し替えてテストを隔離します。
// --- 関数のモック ---
describe('jest.fn() の使い方', () => {
it('コールバック関数をモックする', () => {
const mockCallback = jest.fn((x: number) => x * 2);
[1, 2, 3].forEach(mockCallback);
// 呼び出し回数の検証
expect(mockCallback).toHaveBeenCalledTimes(3);
// 引数の検証
expect(mockCallback).toHaveBeenCalledWith(1);
expect(mockCallback).toHaveBeenCalledWith(2);
expect(mockCallback).toHaveBeenCalledWith(3);
// 戻り値の検証
expect(mockCallback.mock.results[0].value).toBe(2);
expect(mockCallback.mock.results[1].value).toBe(4);
});
it('戻り値を制御する', () => {
const mock = jest.fn<(id: number) => string>();
mock
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('default');
expect(mock(1)).toBe('first');
expect(mock(2)).toBe('second');
expect(mock(3)).toBe('default');
expect(mock(4)).toBe('default');
});
});
モジュール全体のモック:
// src/api.ts
export async function fetchUser(id: number): Promise<{ name: string }> {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
// src/userService.ts
import { fetchUser } from './api';
export async function getUserName(id: number): Promise<string> {
const user = await fetchUser(id);
return user.name;
}
// src/__tests__/userService.test.ts
import { getUserName } from '../userService';
import { fetchUser } from '../api';
jest.mock('../api');
const mockedFetchUser = fetchUser as jest.MockedFunction<typeof fetchUser>;
describe('getUserName', () => {
it('ユーザー名を返す', async () => {
mockedFetchUser.mockResolvedValue({ name: 'Taro' });
const name = await getUserName(1);
expect(name).toBe('Taro');
expect(mockedFetchUser).toHaveBeenCalledWith(1);
});
});
3. 非同期テスト
// Promise / async-await
describe('非同期テスト', () => {
// async/await(推奨)
it('async/await パターン', async () => {
const data = await Promise.resolve('hello');
expect(data).toBe('hello');
});
// resolves / rejects マッチャー
it('resolves マッチャー', async () => {
await expect(Promise.resolve('done')).resolves.toBe('done');
});
it('rejects マッチャー', async () => {
await expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');
});
});
4. スナップショットテスト
UIやデータ構造の回帰テストに有効です。
interface UserProfile {
id: number;
name: string;
email: string;
createdAt: string;
}
function createUserProfile(name: string): UserProfile {
return {
id: 1,
name,
email: `${name.toLowerCase()}@example.com`,
createdAt: '2024-01-01T00:00:00.000Z',
};
}
describe('スナップショットテスト', () => {
it('UserProfile の構造が変わっていないこと', () => {
const profile = createUserProfile('Taro');
expect(profile).toMatchSnapshot();
});
// インラインスナップショット(スナップショットがテストファイル内に保存される)
it('インラインスナップショット', () => {
const profile = createUserProfile('Hanako');
expect(profile).toMatchInlineSnapshot(`
{
"createdAt": "2024-01-01T00:00:00.000Z",
"email": "hanako@example.com",
"id": 1,
"name": "Hanako",
}
`);
});
});
スナップショットを更新する場合:
npx jest --updateSnapshot
# または
npx jest -u
5. セットアップ・ティアダウン(ライフサイクル)
describe('ライフサイクルフック', () => {
let db: { connected: boolean; data: string[] };
// 全テストの前に1回だけ実行
beforeAll(() => {
db = { connected: true, data: [] };
console.log('DB接続');
});
// 各テストの前に毎回実行
beforeEach(() => {
db.data = []; // テストごとにデータをリセット
});
// 各テストの後に毎回実行
afterEach(() => {
jest.restoreAllMocks(); // モックのリストア
});
// 全テストの後に1回だけ実行
afterAll(() => {
db.connected = false;
console.log('DB切断');
});
it('データを追加できる', () => {
db.data.push('item1');
expect(db.data).toHaveLength(1);
});
it('データは毎回リセットされている', () => {
expect(db.data).toHaveLength(0);
});
});
類似パッケージとの比較
| 特徴 | Jest | Vitest | Mocha |
|---|---|---|---|
| 設定の手軽さ | ◎ Zero Config | ◎ Vite連携で即使える | △ 別途設定が必要 |
| アサーション | 内蔵 | 内蔵(Jest互換) | 別途必要(Chai等) |
| モック | 内蔵 | 内蔵(Jest互換) | 別途必要(Sinon等) |
| カバレッジ | 内蔵 | 内蔵(c8/istanbul) | 別途必要(nyc等) |
| スナップショット | ◎ 内蔵 | ◎ 内蔵 | △ プラグインが必要 |
| 実行速度 | ○ | ◎ ESM/HMRで高速 | ○ |
| ESMサポート | △ 実験的 | ◎ ネイティブ | ○ |
| エコシステム | ◎ 最大規模 | ○ 急成長中 | ◎ 歴史が長い |
| 主な用途 | React/汎用 | Viteプロジェクト | 柔軟な構成が必要な場合 |
選定の目安:
- Viteを使っている → Vitest が最適
- React/既存の大規模プロジェクト → Jest が安定
- 細かくツールを選びたい → Mocha + Chai + Sinon
注意点・Tips
1. ESMサポートについて
Jest は歴史的に CommonJS ベースで動作します。ESM(import/export)を使う場合は ts-jest や Babel の設定が必要です。
// jest.config.ts で ts-jest を使う場合
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
// ESMモジュールを変換対象に含める
transformIgnorePatterns: [
'node_modules/(?!(esm-only-package)/)',
],
};
注意(v30.x): Jest 30 では Node.js のネイティブ ESM サポートが改善されていますが、エコシステム全体の対応状況を確認してから導入してください。
2. テストの並列実行とパフォーマンス
Jest はデフォルトでテストファイルを並列実行します。CI環境でメモリ不足になる場合は以下を試してください。
# ワーカー数を制限
jest --maxWorkers=2
# 直列実行
jest --runInBand
3. jest.mock() の巻き上げ(Hoisting)
jest.mock() はファイルの先頭に自動的に巻き上げられます