概要
「mock と stub の違いがよく分からない」「Mockito の mock と spy をなんとなく使い分けている」――テストダブルの分類はテスト設計の基礎でありながら、現場では曖昧に扱われがちです。Gerard Meszaros が整理した 5 種のテストダブル(Dummy、Stub、Spy、Mock、Fake)には、それぞれ明確な目的と適用場面があります。この記事では、通知サービス(メール送信・ログ記録)を題材に、5 種のテストダブルをそれぞれ手動実装と Mockito の両方で書き分けます。「このテストではどのテストダブルを使うべきか」の判断基準を、コードで実感できるように整理します。
使いどころ
メール送信サービスのテストで、実際にメールを送信せずに Mock で呼び出し引数を検証する
外部 API クライアントのテストで Stub を差し込み、ビジネスロジックを検証する
HashMap ベースの Fake リポジトリで CRUD 処理を高速に検証する
ロガーや監査出力の記録だけ確認したい場面で Spy を使い、実際の副作用を避ける
依存先の有無だけを埋めたいケースで Dummy を使い、テストの意図を明確にする
コード例
import org.junit.jupiter.api.*;
import org.mockito.ArgumentCaptor;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class NotificationServiceTest {
interface NotificationSender { boolean send(String to, String message); }
interface AuditLogger { void log(String event); }
record NotificationService(NotificationSender sender, AuditLogger logger) {
boolean notify(String to, String message) {
boolean result = sender.send(to, message);
if (result) logger.log("送信成功: " + to);
return result;
}
}
@Nested @DisplayName("Dummy: 使われない引数を埋める")
class DummyTests {
@Test void dummyLogger() {
NotificationSender stub = (to, msg) -> true;
AuditLogger dummy = event -> {};
assertTrue(new NotificationService(stub, dummy).notify("[email protected]", "テスト"));
}
}
@Nested @DisplayName("Stub: 固定値を返す")
class StubTests {
@Test void stubSender() {
NotificationSender stub = (to, msg) -> true;
assertTrue(new NotificationService(stub, e -> {}).notify("[email protected]", "お知らせ"));
}
}
@Nested @DisplayName("Spy: 呼び出し記録")
class SpyTests {
@Test void manualSpy() {
List<String> history = new ArrayList<>();
AuditLogger spy = history::add;
new NotificationService((to, msg) -> true, spy).notify("[email protected]", "重要連絡");
assertEquals(1, history.size());
assertEquals("送信成功: [email protected]", history.get(0));
}
}
@Nested @DisplayName("Mock: 呼び出しを検証")
class MockTests {
@Test void mockVerification() {
var mockSender = mock(NotificationSender.class);
when(mockSender.send(anyString(), anyString())).thenReturn(true);
var mockLogger = mock(AuditLogger.class);
new NotificationService(mockSender, mockLogger).notify("[email protected]", "障害通知");
var captor = ArgumentCaptor.forClass(String.class);
verify(mockSender).send(captor.capture(), eq("障害通知"));
assertEquals("[email protected]", captor.getValue());
verify(mockLogger).log("送信成功: [email protected]");
}
}
@Nested @DisplayName("Fake: 簡易実装")
class FakeTests {
static class FakeSender implements NotificationSender {
final List<String> sent = new ArrayList<>();
@Override public boolean send(String to, String msg) { sent.add(to + ":" + msg); return true; }
}
@Test void fakeSender() {
var fake = new FakeSender();
var svc = new NotificationService(fake, e -> {});
svc.notify("[email protected]", "レポート");
svc.notify("[email protected]", "承認依頼");
assertEquals(2, fake.sent.size());
}
}
}Version Coverage
record をテストデータや Stub の戻り値に使うことで可読性が上がる。
// Java 17: ラムダで Stub を1行で定義 NotificationSender stubSender = (to, msg) -> true;
Library Comparison
注意点
Mockito の mock() で未定義のメソッドを呼ぶとデフォルト値(null 等)が返る。NullPointerException の原因になる
spy() は実メソッドを呼ぶため、副作用のあるメソッドには doReturn を使う
verify() の多用はテストを実装に密結合させる。戻り値で検証できるケースでは assertEquals を優先する
Fake の振る舞いが本番と乖離すると信頼性が下がる。定期的に確認するか Testcontainers で補完する
Dummy と Stub を明確に区別せず何でも mock で済ませると、テストの役割分担が見えにくくなる
Fake が便利だからと本番実装の代わりに使い続けると、制約や例外処理の差を見逃しやすい
FAQ
Stub は固定値を返すだけで呼び出し検証はしません。Mock は verify でメソッドの呼び出しを検証します。
HashMap で CRUD を再現する Fake リポジトリが代表例です。外部依存を排除しつつ状態を持つテストに使います。
mock は白紙の代役で全メソッドがデフォルト値。spy は実オブジェクトを包み一部だけ差し替えたいときに使います。
はい。今回のテストでは使わない依存を埋めるだけの場面で自然に登場します。名前を付けて意識すると、必要以上に複雑なモックを書かずに済みます。
状態を持つ簡易リポジトリやインメモリ実装なら手動 Fake が分かりやすいです。呼び出し回数や引数検証が中心なら Mockito の方が短く書けます。