モック
テスト時に外部依存を模倣するオブジェクトで、テストの独立性と速度を確保する
モックとは
モック (Mock) は、テスト時に外部依存 (DB、API、ファイルシステム) を模倣するオブジェクトで、テストの独立性と速度を確保する。実際の DB に接続せずにテストを実行でき、ネットワーク障害などの異常系もシミュレートできる。
テストダブルの種類
テストダブルの種類を以下にまとめる。
| 種類 | 説明 | 例 |
|---|---|---|
| Dummy | 引数を埋めるために渡されるだけで、実際には使われない | 空オブジェクト、null |
| Stub | 決め打ちの値を返す。仕込んだ範囲の外には応答しない | vi.fn().mockReturnValue(42) |
| Spy | Stub に呼び出しの記録を足したもの | vi.spyOn(obj, 'method') |
| Mock | 期待する呼び出しをあらかじめ仕込み、その通りに呼ばれたかを検証 | vi.fn() |
| Fake | 動く実装を持つが、本番には使えない近道をしている | InMemoryUserRepository |
この 5 分類は Gerard Meszaros の書籍 xUnit Test Patterns: Refactoring Test Code (Addison-Wesley、2007 年) が整理した語彙で、Martin Fowler の記事 Mocks Aren't Stubs もこの用語を踏襲している。5 種のうち「期待通りに呼ばれたか」の検証を成立条件とするのは Mock だけで、残りは戻り値や最終状態を確かめる使い方が主になる。分類名を借りるだけでなく、テストが何を確かめているのか (呼び出し手順か、結果か) を意識して選ぶ。
なお Vitest の vi.spyOn は既定で元の実装を呼び続ける (2026 年 8 月時点の Vitest 4.1 で実測)。記録だけ取りたいのか、実装まで差し替えたいのかで mockImplementation を足すかを決める。
Vitest でのモック
Vitest でのモックのコード例を示す。
import { vi, describe, it, expect } from 'vitest';
import { createOrder } from './order';
import { getUser } from './db';
// モジュールのモック: 差し替えたい関数を vi.fn() で置き換える
vi.mock('./db', () => ({ getUser: vi.fn() }));
describe('OrderService', () => {
it('ユーザーが存在する場合に注文を作成する', async () => {
// 戻り値の設定も検証も、テスト対象が実際に呼ぶモック済みモジュール側に対して行う
vi.mocked(getUser).mockResolvedValue({ id: '1', name: 'Alice' });
const result = await createOrder('1', [{ productId: 'P1', qty: 2 }]);
expect(result.status).toBe('created');
expect(getUser).toHaveBeenCalledWith('1');
});
});
ここで踏みやすい落とし穴が 2 つある。1 つは検証対象の取り違えで、vi.fn() をローカル変数に持って expect すると、テスト対象が呼ぶのはモック済みモジュール側の別の関数なので呼び出しが記録されず、検証が空振りする。もう 1 つは巻き上げで、vi.mock はファイル先頭へ移されるためファクトリの中から外側の変数を参照すると Cannot access '...' before initialization で落ちる。共有したい値は vi.hoisted() の戻り値として定義する。
モックしすぎの問題
モックしすぎの問題のコード例を示す。
// ❌ モックしすぎ: テストが実装の詳細に依存
it('should call db.put with correct params', () => {
createUser({ name: 'Alice' });
expect(db.put).toHaveBeenCalledWith({
TableName: 'users',
Item: { id: expect.any(String), name: 'Alice' },
});
});
// → db.put の引数が変わるとテストが壊れる (実装の詳細)
// ✅ 振る舞いをテスト
it('should create a user and return it', async () => {
const user = await createUser({ name: 'Alice' });
expect(user.name).toBe('Alice');
expect(user.id).toBeDefined();
});
Fake (インメモリ実装)
Fake (インメモリ実装) のコード例を示す。
class InMemoryUserRepository implements UserRepository {
private store = new Map<string, User>();
async findById(id: string) { return this.store.get(id) ?? null; }
async save(user: User) { this.store.set(user.id, user); }
}
// テストで使用
const repo = new InMemoryUserRepository();
const service = new UserService(repo);
Fake はモックより実装に近いため、呼び出し手順ではなく最終状態でテストを書ける。ただし Fake 自身の振る舞いが本物とずれると、テストは通るのに本番で壊れる。同じ契約を検証するテストを Fake と本物の両方に流せる形にしておくと、このずれを早く見つけられる。
モックの判断基準
モックの判断基準を以下にまとめる。
| ケース | 推奨 |
|---|---|
| 外部 API (HTTP) | モックする |
| DB (DynamoDB) | Fake (InMemory) or モック |
| 純粋関数 | モックしない (そのまま呼ぶ) |
時刻 (Date.now()) | モックする (vi.useFakeTimers()) |
モックを扱う関連書籍も多い。
この記事は役に立ちましたか?
関連用語
関連する記事
テスト本ガイド - テスト設計を学べる技術書の選び方
テストの書き方からテスト戦略まで学べる技術書の選び方を紹介。テストピラミッド、TDD の正しい読み方、テストの ROI の考え方を解説します。
「動くコード」と「良いコード」の間にある本
コードが動くようになった後、次に何を学べばよいのか。「動くコード」を「良いコード」に変えるために必要な知識と、それを効率的に学べる本の選び方を解説します。
バグを生むのは知識不足ではなく想像力不足である
バグの多くは、コードを書いた時点で「こういうケースもありうる」と想像できなかったことが原因です。想像力を鍛える読書法と、エッジケースへの感度を高める方法を解説します。