はじめに
単体テストを書くときに、モックやスパイなどが出てきますが、どう使い分けたら良いのか理解できていないのでまとめました。
Test Doubleとは
Test Double(テストダブル) とは、テスト中に実装のテストが必要な部分を置き換えるために使用される代替物のことです。
単体テストでは、データベース接続、外部APIへの通信、ファイルシステムへのアクセスなど、外部依存を切り離し、1つの機能に絞ってテストする必要があります。これらの依存をそのままにしておくと、テストが不安定になり、実行速度が遅く、失敗したときに原因特定が難しくなります。
Test Doubleの種類
Test Doubleには複数の種類があります。もっとも広く使われている分類はジェラルド・メサローシュによる次の5つの分類です。
| 種類 | 主な役割 |
|---|---|
| ダミー (Dummy) | 引数として渡すだけで、使用されない |
| フェイク (Fake) | 簡略化された動作を伴う実装を提供する |
| スタブ (Stub) | 呼び出しに対して、あらかじめ決められた値を返す |
| モック (Mock) | 期待値を設定し、呼び出しの振る舞いを検証する |
| スパイ (Spy) | 本物の実装は生かしつつ、呼び出し履歴を記録・監視する |
1. スタブ(Stub)
スタブ(Stub) とは、外部依存の呼び出しに対して、決められた値を返すだけのダミーオブジェクトです。テストが必要な戻り値さえあれば良い場合に使います。メソッドが呼ばれたかどうかは気にしません。
// ユーザー情報を取得する関数をテストする
interface User {
id: number;
name: string;
email: string;
}
interface UserRepository {
findById(id: number): Promise<User | null>;
}
// スタブ:常に決まったユーザーを返す
const stubUserRepository: UserRepository = {
findById: async (id: number) => ({
id: 1,
name: 'テストユーザー',
email: 'test@example.com'
})
};
// テスト対象
const getUserWithGreeting = async (repo: UserRepository, id: number) => {
const user = await repo.findById(id);
return user ? `こんにちは、${user.name}さん!` : null;
};
// テスト
describe('getUserWithGreeting', () => {
it('ユーザーが見つかったとき、挨拶を返す', async () => {
const result = await getUserWithGreeting(stubUserRepository, 1);
expect(result).toBe('こんにちは、テストユーザーさん!');
});
});
2. モック(Mock)
モック(Mock) とは、外部依存の呼び出しに対して、決められた値を返しつつ、「正しく呼ばれたかどうか」も検証するオブジェクトです。スタブの機能に加えて、呼び出しの検証機能を備えています。期待値をあらかじめ設定しておき、その期待値に基づいて実際の呼び出しが正しく行われたかを検証します。
// メール送信を含むユーザー作成機能をテストする
interface EmailService {
send(email: string, subject: string, body: string): Promise<void>;
}
const createUserAndSendEmail = async (
repo: UserRepository,
emailService: EmailService,
userData: { name: string; email: string }
) => {
const user = { id: 1, ...userData };
// ユーザーを保存(ここではスキップ)
await emailService.send(
user.email,
'ウェルカムメール',
`${user.name}さん、ようこそ!`
);
return user;
};
// モック:期待値を設定して呼び出しを検証
describe('createUserAndSendEmail', () => {
it('メールが正しい内容で送信される', async () => {
// ユーザー保存用のモック
const userRepoMock: UserRepository = {
findById: jest.fn().mockResolvedValue(null)
};
// メール送信用のモック
const emailServiceMock = {
send: jest.fn().mockResolvedValue(undefined)
};
// テスト対象を実行
await createUserAndSendEmail(
userRepoMock,
emailServiceMock,
{ name: '太郎', email: 'taro@example.com' }
);
// 期待値:メールが正しい引数で呼ばれたかを検証
expect(emailServiceMock.send).toHaveBeenCalledWith(
'taro@example.com',
'ウェルカムメール',
'太郎さん、ようこそ!'
);
});
it('メールは1回だけ送信される', async () => {
const userRepoMock: UserRepository = {
findById: jest.fn().mockResolvedValue(null)
};
const emailServiceMock = {
send: jest.fn().mockResolvedValue(undefined)
};
await createUserAndSendEmail(
userRepoMock,
emailServiceMock,
{ name: '太郎', email: 'taro@example.com' }
);
// 期待値:メール送信が正確に1回呼ばれたかを検証
expect(emailServiceMock.send).toHaveBeenCalledTimes(1);
});
it('メール送信に失敗した場合はエラーが伝播する', async () => {
const userRepoMock: UserRepository = {
findById: jest.fn().mockResolvedValue(null)
};
// 期待値:メール送信がエラーを返す設定
const emailServiceMock = {
send: jest.fn().mockRejectedValue(new Error('送信失敗'))
};
// テスト対象を実行してエラーが伝播することを検証
await expect(
createUserAndSendEmail(
userRepoMock,
emailServiceMock,
{ name: '太郎', email: 'taro@example.com' }
)
).rejects.toThrow('送信失敗');
// 期待値:メール送信が呼ばれたことを検証
expect(emailServiceMock.send).toHaveBeenCalled();
});
});
3. スパイ(Spy)
スパイ(Spy) とは、実装のほぼすべてを本物のまま保ちつつ、呼び出しを記録して検証できるオブジェクトです。モックとは違い、実装は本物のまま動作しながら、呼び出し履歴を記録します。
// ロギング機能付きのサービスをテストする
class UserService {
constructor(private logger: Logger) {}
createUser = (name: string) => {
this.logger.log(`ユーザー作成: ${name}`);
return { id: 1, name };
};
}
interface Logger {
log(message: string): void;
}
describe('UserService', () => {
it('ユーザー作成時にログが記録される', () => {
// 本物の Logger 実装
const logger: Logger = {
log: (message: string) => {
console.log(`[LOG] ${message}`);
}
};
// Logger をスパイして、実装は保ちながら呼び出しを監視
const logSpy = jest.spyOn(logger, 'log');
const userService = new UserService(logger);
userService.createUser('田中');
// 実装は本物のまま動作しつつ、呼び出しが記録されているか検証
expect(logSpy).toHaveBeenCalledWith('ユーザー作成: 田中');
// console.log も実際に実行されている
logSpy.mockRestore();
});
});
まとめ
- テスト中に外部依存を置き換えるための代替物をTest Doubleと呼ぶ。
- スタブとは、決められた値を返すだけのシンプルなダミーオブジェクトのことで、戻り値さえあれば良い場合に使う。
- モックとは、戻り値を返すだけでなく、メソッドの呼び出し方も検証するオブジェクトのことで、メール送信や外部API呼び出しなど副作用が重要なテストで活躍する。
- スパイとは、実装を本物のまま保ちながら、呼び出し履歴だけを記録するオブジェクトのことで、実装を生かしたままテストしたい場合に有効。