概要

「mock と stub の違いがよく分からない」「Mockito の mock と spy をなんとなく使い分けている」――テストダブルの分類はテスト設計の基礎でありながら、現場では曖昧に扱われがちです。Gerard Meszaros が整理した 5 種のテストダブル(Dummy、Stub、Spy、Mock、Fake)には、それぞれ明確な目的と適用場面があります。この記事では、通知サービス(メール送信・ログ記録)を題材に、5 種のテストダブルをそれぞれ手動実装と Mockito の両方で書き分けます。「このテストではどのテストダブルを使うべきか」の判断基準を、コードで実感できるように整理します。

使いどころ

メール送信サービスのテストで、実際にメールを送信せずに Mock で呼び出し引数を検証する

外部 API クライアントのテストで Stub を差し込み、ビジネスロジックを検証する

HashMap ベースの Fake リポジトリで CRUD 処理を高速に検証する

ロガーや監査出力の記録だけ確認したい場面で Spy を使い、実際の副作用を避ける

依存先の有無だけを埋めたいケースで Dummy を使い、テストの意図を明確にする

コード例

NotificationServiceTest.java
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());
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record をテストデータや Stub の戻り値に使うことで可読性が上がる。

Java 17
// Java 17: ラムダで Stub を1行で定義
NotificationSender stubSender = (to, msg) -> true;

Library Comparison

Mockitoテストダブルの定番。when/verify のシンプルな API で学習コストが低い。過度にモックを使うとテストが実装に密結合し、リファクタリング時に大量修正が発生する。
手動実装(Pure Java)振る舞いを明示的に制御したいとき。Fake リポジトリなど状態を持つテストダブルに向く。テストダブルのクラスが増えて管理コストが上がる。
EasyMockレガシープロジェクトで既に採用されている場合。Mockito に比べて記述が冗長。新規で選ぶ理由はほぼない。

注意点

Mockito の mock() で未定義のメソッドを呼ぶとデフォルト値(null 等)が返る。NullPointerException の原因になる

spy() は実メソッドを呼ぶため、副作用のあるメソッドには doReturn を使う

verify() の多用はテストを実装に密結合させる。戻り値で検証できるケースでは assertEquals を優先する

Fake の振る舞いが本番と乖離すると信頼性が下がる。定期的に確認するか Testcontainers で補完する

Dummy と Stub を明確に区別せず何でも mock で済ませると、テストの役割分担が見えにくくなる

Fake が便利だからと本番実装の代わりに使い続けると、制約や例外処理の差を見逃しやすい

FAQ

Stub と Mock の一番の違いは何ですか。

Stub は固定値を返すだけで呼び出し検証はしません。Mock は verify でメソッドの呼び出しを検証します。

Fake はどのような場面で使いますか。

HashMap で CRUD を再現する Fake リポジトリが代表例です。外部依存を排除しつつ状態を持つテストに使います。

mock と spy の使い分けは。

mock は白紙の代役で全メソッドがデフォルト値。spy は実オブジェクトを包み一部だけ差し替えたいときに使います。

Dummy は実務で本当に使いますか。

はい。今回のテストでは使わない依存を埋めるだけの場面で自然に登場します。名前を付けて意識すると、必要以上に複雑なモックを書かずに済みます。

手動 Fake と Mockito のどちらを選ぶべきですか。

状態を持つ簡易リポジトリやインメモリ実装なら手動 Fake が分かりやすいです。呼び出し回数や引数検証が中心なら Mockito の方が短く書けます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます