Mock、Stub、Fakeはどう違うのか

1. 概要

ユニットテストを書くとき、データベース、決済ゲートウェイ、メール配信サービスなどの実際の依存関係を「代役」オブジェクトで置き換えることがよくあります。これらのオブジェクトを総称して テストダブル と呼びます。多くの人はすべてを「mock」と呼びますが、mock、stub、fakeは3つの異なる目的を果たします。

この記事では、3つのタイプを同じ例で区別し、どのタイプをいつ使うべきか、そしてテストをぼろぼろにしたり無意味にしたりするミスを避ける方法を理解するのに役立ちます。

2. 本記事を通じて使用する例

2.1. 依存関係

本記事ではJUnit 5、AssertJ、Mockitoを使用します。Spring Boot 3.xを使っているなら、spring-boot-starter-testに3つすべてが含まれています。そうでなければ、以下の依存関係を追加してください:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.11.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.26.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>5.14.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2. テスト対象のコード

シンプルなOrderServiceがあります。このサービスは代金を請求し、注文を保存してから確認メールを送信します:

public record Order(String id, String customerEmail, BigDecimal amount) {}

public interface PaymentGateway {
    boolean charge(String customerEmail, BigDecimal amount);
}

public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(String id);
}

public interface EmailSender {
    void send(String to, String subject);
}

public class OrderService {

    private final PaymentGateway paymentGateway;
    private final OrderRepository orderRepository;
    private final EmailSender emailSender;

    public OrderService(PaymentGateway paymentGateway,
                        OrderRepository orderRepository,
                        EmailSender emailSender) {
        this.paymentGateway = paymentGateway;
        this.orderRepository = orderRepository;
        this.emailSender = emailSender;
    }

    public boolean placeOrder(Order order) {
        if (!paymentGateway.charge(order.customerEmail(), order.amount())) {
            return false;
        }
        orderRepository.save(order);
        emailSender.send(order.customerEmail(), "ご注文" + order.id() + "が確認されました");
        return true;
    }
}

OrderServiceには3つの依存関係があります。各依存関係は異なるタイプのテストダブルに適しています:

  • PaymentGateway は入力データを提供する (決済結果)ため、stubに適しています。
  • EmailSender は副作用 (メール送信)で検証が必要なため、mockに適しています。
  • OrderRepository は実際のストレージ動作を持つため、fakeに適しています。

3. Stub: 事前に決めた値を返す

Stub は呼び出しに対して固定値を返すオブジェクトです。Stubは何回呼び出されてもかまいません。Stubに対してアサーションもしません。Stubの唯一の目的は、テスト対象コードを検査したいロジックの正しいブランチに導くことです。

3.1. 手書きのStub

最も簡単な方法は、インターフェースを自分で実装することです:

class DecliningPaymentGateway implements PaymentGateway {
    @Override
    public boolean charge(String customerEmail, BigDecimal amount) {
        return false;
    }
}

このクラスは常に支払いを拒否します。「支払い失敗」ブランチを確認するために使用します:

class OrderServiceStubTest {

    @Test
    void givenDeclinedPayment_whenPlaceOrder_thenReturnFalse() {
        OrderService service = new OrderService(
                new DecliningPaymentGateway(),
                mock(OrderRepository.class),
                mock(EmailSender.class));

        boolean result = service.placeOrder(
                new Order("ORD-1", "an@example.com", new BigDecimal("150000")));

        assertThat(result).isFalse();
    }
}

このテストでは:

  • DecliningPaymentGateway はstubです。placeOrder()をreturn falseブランチに強制します。
  • アサーションはサービスの戻り値上にあり、stubの上にはありません。
  • mock(OrderRepository.class)とmock(EmailSender.class)はここでは単にダミーの役割を果たします。これらはコンストラクタパラメータを埋めるために渡されるオブジェクトで、使用されません。

3.2. Mockitoを使ったStub

各シナリオに対して1つのクラスを書くのはすぐに面倒になります。Mockitoではテスト内で直接stubを作成できます:

@Test
void givenApprovedPayment_whenPlaceOrder_thenReturnTrue() {
    PaymentGateway paymentGateway = mock(PaymentGateway.class);
    when(paymentGateway.charge(anyString(), any(BigDecimal.class))).thenReturn(true);

    OrderService service = new OrderService(
            paymentGateway, mock(OrderRepository.class), mock(EmailSender.class));

    assertThat(service.placeOrder(
            new Order("ORD-2", "binh@example.com", new BigDecimal("99000"))))
            .isTrue();
}

mock()関数で作成されていても、ここのpaymentGatewayは役割としてはまだstubです: when(...).thenReturn(...)で戻り値を設定するだけで、verifyすることはありません。ライブラリの関数名がテストダブルのタイプを決定しません。そのオブジェクトの使い方が決定要因です。

4. Mock: 相互作用を検証する

Mock は呼び出しを記録するオブジェクトで、その後コードが正しいメソッドを正しいパラメータで正しい回数呼び出したかどうかを確認できます。stubと異なり、アサーションはmock自体に対してです。

Mockはメール送信、イベント発行、または監査ログの記録など、何も返さない副作用の動作を確認する場合に便利です。

@ExtendWith(MockitoExtension.class)
class OrderServiceMockTest {

    @Mock
    PaymentGateway paymentGateway;

    @Mock
    OrderRepository orderRepository;

    @Mock
    EmailSender emailSender;

    @InjectMocks
    OrderService orderService;

    @Test
    void givenApprovedPayment_whenPlaceOrder_thenSendConfirmationEmail() {
        when(paymentGateway.charge("an@example.com", new BigDecimal("150000")))
                .thenReturn(true);

        orderService.placeOrder(new Order("ORD-1", "an@example.com", new BigDecimal("150000")));

        verify(emailSender).send("an@example.com", "ご注文ORD-1が確認されました");
    }

    @Test
    void givenDeclinedPayment_whenPlaceOrder_thenNoEmailSent() {
        when(paymentGateway.charge(anyString(), any(BigDecimal.class))).thenReturn(false);

        orderService.placeOrder(new Order("ORD-1", "an@example.com", new BigDecimal("150000")));

        verifyNoInteractions(emailSender, orderRepository);
    }
}

各部分を分析すると:

  • @ExtendWith(MockitoExtension.class)はJUnit 5でMockitoをアクティブにします。@Mockフィールドは各テストの前に初期化されます。
  • @InjectMocksはOrderServiceを作成し、3つの@Mockオブジェクトをコンストラクタ経由でインジェクトします。
  • paymentGatewayはまだstubですが、emailSenderはverifyするため真のmockです。
  • verify(emailSender).send(...)はsend()が正確に1回正しい2つのパラメータで呼ばれたことをチェックします。
  • verifyNoInteractions(...)は支払い失敗時、システムが注文を保存せず、メールも送信しないことを主張します。

もしOrderServiceのemailSender.send(...)行を誤って削除すると、最初のテストは次のようなメッセージで失敗します:

Wanted but not invoked:
emailSender.send(
    "an@example.com",
    "ご注文ORD-1が確認されました"
);
-> at OrderServiceMockTest.givenApprovedPayment_whenPlaceOrder_thenSendConfirmationEmail

Actually, there were zero interactions with this mock.

注意: 同じ呼び出しをstubとverifyで両方しないでください。例えばwhen(paymentGateway.charge(...))その後verify(paymentGateway).charge(...)です。stubが呼ばれなければ、サービスはtrueを返さず、テストは別の場所で失敗します。verifyコマンドはここでは余分で、単にテストを実装に結びつけるだけです。

5. Fake: 実装は本物だが簡略化

Fake は実際に機能する実装ですが、本番環境には適さないショートカットを使用します。典型的な例は、データベース代わりにHashMapに保存するリポジトリです:

public class InMemoryOrderRepository implements OrderRepository {

    private final Map<String, Order> storage = new HashMap<>();

    @Override
    public void save(Order order) {
        storage.put(order.id(), order);
    }

    @Override
    public Optional<Order> findById(String id) {
        return Optional.ofNullable(storage.get(id));
    }
}

stubと異なり、fakeはロジックと状態を持ちます。save()を呼んでからfindById()を呼ぶと、保存したばかりの正しい注文が返されます。このため、呼び出しをチェックする代わりに最終状態をチェックします:

class OrderServiceFakeTest {

    private final InMemoryOrderRepository orderRepository = new InMemoryOrderRepository();

    private final OrderService orderService = new OrderService(
            (email, amount) -> true,      // stub形式のlambda
            orderRepository,               // fake
            (to, subject) -> { });         // ダミー形式のlambda

    @Test
    void givenApprovedPayment_whenPlaceOrder_thenOrderIsPersisted() {
        Order order = new Order("ORD-3", "chi@example.com", new BigDecimal("250000"));

        orderService.placeOrder(order);

        assertThat(orderRepository.findById("ORD-3")).contains(order);
    }
}

注目すべき点が何個かあります:

  • PaymentGatewayとEmailSenderは各々1つのabstractメソッドしかないため、Mockitoなしでlambdaを使ってstubとダミーを作成します。
  • テストはOrderServiceがsave()を何回呼ぶか、どの順番で呼ぶかは知りません。テストは注文を置いた後、注文がストレージに入っているかだけを気にします。
  • 後でOrderServiceを別のメソッド(例えばsaveAll())を使うようにリファクタリングしても、fakeを使ったテストは最終結果が正しい限り合格します。verify(orderRepository).save(order)を使うテストは壊れます。

注意: Fakeはコードなので、fakeもバグを持つ可能性があります。fakeが実装と異なる動作をすれば (例えば重複キーでエラーをスロー しない)、テストは合格し本番は失敗する可能性があります。重要なfakeについては、fakeと実装の間で共有される契約テスト (例えば Testcontainers で実行される JPA バージョン) を書いてください。

6. 比較と選択方法

以下の表は違いをまとめています:

項目 Stub Mock Fake
目的 入力データを提供 相互作用を検証 実際の動作を軽量に置き換え
ロジックがあるか いいえ、固定値を返す いいえ、呼び出しを記録するだけ はい、実装のように動作
Assertはどこに テスト対象コードの結果上 mock自体上 (verify) fakeの状態上
テストのタイプ State-based Interaction-based State-based
リファクタリング時の耐久性 高い より低い 高い

これら3つ以外に、他の2つのタイプにも遭遇します:

  • ダミー: パラメータを埋めるために渡されるオブジェクトで、実際には使用されません。
  • スパイ: 実オブジェクトをラップし、依然として実ロジックを実行しますがverifyするために呼び出しを記録します。Mockitoでは、spy(...)を使ってスパイを作成します。

選択のための簡単なルール:

  1. 依存関係があなたのコードにデータを返す場合は、stubを使用します。
  2. 依存関係が外部への副作用 (メール、メッセージキュー、サードパーティAPI) で、その状態を観察する方法がない場合は、mockを使用します。
  3. 依存関係が状態を持つ場合、また多くのテストで使用される場合 (リポジトリ、キャッシュ、クロック)、fakeを1回書いて複数の場所で再利用することを検討してください。

一般的に、相互作用をチェック(mock)するより状態をチェック(stub、fake)を優先します。状態ベースのテストは結果を説明し、相互作用ベースのテストはやり方を説明し、やり方はより頻繁に変わります。

7. よくある誤り

すべてをmockする。 テストに5つのwhenステートメントと4つのverifyステートメントがあるなら、そのテストは実装をコピーしており、動作をチェックしていません。2行のコードの順序を変えるとテストが失敗します、たとえ結果がまだ正しくても。

Value objectをmockする。 Order、BigDecimal、LocalDateをmockしないでください。実インスタンスを作成してください。Mockito 5はデフォルトでfinalクラスとrecordをmockできますが、できるからといってそうすべきではありません。

所有しないタイプをmockする。 RestTemplateまたはベンダーSDKを直接mockするのは、テストがライブラリの動作について間違ったことを想定させます。ライブラリをあなた自身のインターフェース (PaymentGatewayなど) の後ろにラップしてから、そのインターフェースをmockしてください。実装はintegrationテストで確認されます。

不要なstub。 MockitoExtensionでは、Mockitoはデフォルトで厳密なstubsを使用します。コードが実行しない呼び出しをstubすると、テストは失敗します:

org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.

このメッセージでlenient()を使って警告を無効にしないでください。このメッセージは通常、テストが思っているブランチに入っていないか、セットアップが古いことを示します。

fakeをintegrationテストの代わりに使用する。 InMemoryOrderRepositoryはSQL、制約、またはトランザクションをチェックできません。Fakeはユニットテストを高速化するのに役立ちますが、実データベースを使ったいくつかのintegrationテストが依然として必要です。

8. まとめ

Stub、mock、fakeはすべてテストダブルですが、3つの異なる質問に答えます。Stubは「依存関係は何を返すか?」。Mockは「コードは依存関係を正しい方法で呼ぶか?」。Fakeは「実装の軽量版があれば、最終結果は何か?」

依存関係のロールに基づいてテストダブルのタイプを選択してください、ツールに基づいてではなく: mock()で作成されたオブジェクトは完全にstubであることができます。状態のチェックを優先し、verifyは本当に検証が必要な副作用にのみ使用してください。テストが短くなり、読みやすくなり、リファクタリング中に壊れにくくなります。

testingmockitojunit5unit-testjava