🥷

依存コンポーネントの代役-テストダブルあるいはモック

に公開

はじめに

テストダブルとはテスト中のテスト対象が依存するコンポーネントを置き換えるコンポーネントです。モックやスタブなどはテストダブルの一種になります。

テストダブルの「ダブル」は危険なスタントやアクションシーンを演じる代役・スタンドダブルの「ダブル」から来ています。

モックやスタブという概念をまとめてテストダブルとして整理したのは Gerard Meszaros氏が2007年に出版したxUnit Test Patterns: Refactoring Test Codeになります。

この記事では、Gerard Meszaros 氏が整理した古典的なテストダブルを確認し、その後、実世界でその定義が使用されているか、あるいはどのような違いがあるかを確認します。

この記事は、なんらかのプログラミング言語で自動テストを記載したことのある読者を想定しています。

古典的なテストダブル

2000年代中頃にGerard Meszaros氏はxUnit Test Patterns: Refactoring Test Codeでテストダブルという概念を紹介しました。

テストは通常、テストコードが テスト対象 に対して直接入力(引数や初期状態)を与えて実行します。しかし実際の処理では、それ以外にもテスト対象と依存コンポーネントの間で 間接入力・間接出力 が発生します。

間接入力・間接出力

間接出力 (indirect output)
テスト実行中に、テスト対象が依存コンポーネントや外部環境へ行う効果で、テスト対象の公開インターフェイス(戻り値や公開状態)からは直接観測できない出力を指す。

間接入力 (indirect input)
テストがテスト対象を呼び出すときに、引数や初期状態としては直接渡していないのに、テスト実行中にテスト対象が依存コンポーネントや外部環境から受け取る入力のこと。

これらの間接入力の制御や、間接出力の観測を行うための役割を担うのが「テストダブル」です。

間接入力を制御する場合は、テスト中にテスト対象に依存するコンポーネントを置き換えてテストが続行できるような値をテスト対象に入力します。
間接入力

間接出力を観測する場合は、テスト中にテスト対象に依存するコンポーネントを置き換えてテスト対象が間接出力する内容を保持して、テストで検証できるようにします。
間接出力

テストダブルは以下の種類に分類できます。

テストダブルのクラス図

  • ダミーオブジェクト: テスト対象に引数として渡されるが実際には使用されないオブジェクト
  • テストスタブ: 依存コンポーネントを置き換え、テストが間接入力を制御できるようにするオブジェクト
  • テストスパイ: テストスタブの機能に加えて間接出力を全て記録する機能を有す
  • モックオブジェクト:テストスタブの機能に加えて、間接出力が期待どおりに行われたかを検証する機能(事前期待と照合)を有す
  • フェイクオブジェクト: 依存コンポーネントの機能と同じ機能の代替実装に置き換えるオブジェクト

なお、この書籍のこの分類は、特定のテストダブルライブラリを前提にしておらず、「むしろ読者自身がテストダブルを実装する/適切に分類するための設計指針になっています。
そのため、後述するとおり、現実世界のテストダブルライブラリの概念とはわずかな差異が発生するケースがあります。

ダミーオブジェクト

ダミーオブジェクトはテスト対象に引数として渡されるが実際には使用されないオブジェクトです。

次の例では、pythonでのダミーオブジェクトを使用したテスト例になります。
必須引数を満たすために、DummyLoggerを渡していますが、ここでテストしているコードではDummyLoggerが使用されることは想定されていません。

テスト対象

from abc import ABC, abstractmethod
class LoggerBase(ABC):
    @abstractmethod
    def info(self, msg: str):
        pass
    @abstractmethod
    def error(self, msg: str):
        pass

class FileLogger(LoggerBase):
    def info(self, msg: str):
        # ... 実装

    def error(self, msg: str):
        # ... 実装

class TargetService:
    def __init__(self, logger: LoggerBase) -> None:
        self._logger = logger
        self._cache = {}

    def get_cache(self, id: str) -> dict:
        # このメソッドでは logger を一切使わないとする
        return self._cache.get(id)

テストコード

class DummyLogger(LoggerBase):
    def info(self, msg: str):
        pass
    def error(self, msg: str):
        pass

def test_get_cache():
    dummy_logger = DummyLogger()
    service = TargetService(dummy_logger)

    service._cache = {"42": "data1"}
    act = service.get_cache("42")
    assert act == "data1"

テストスタブ

依存コンポーネントを置き換え、テストが間接入力を制御できるようにするオブジェクトです。
通常はテストスタブの戻り値を介して間接入力を制御します。
テストスタブを使用することで、テスト対象中の実行が困難な経路でも実行が可能になります。

以下はpythonのpytestでテストスタブを用いた間接入力の制御を行うサンプルとなります。
この例では依存コンポーネントDatabaseClientを、テストに都合のいいデータを返すStubDatabaseClientに置き換えることで間接入力の制御をしています。

依存コンポーネント example/external.py

class DatabaseClient:
    def fetch_user(self, user_id: str) -> dict:
        # 本来はDBアクセス
        raise RuntimeError("real DB call!")

テスト対象 example/service.py

from .external import DatabaseClient

def load_display_name(user_id: str) -> str:
    client = DatabaseClient()
    row = client.fetch_user(user_id)
    return row["display_name"]

テストコード

import pytest

from example import service
from example.external import DatabaseClient

# テストスタブ: 間接入力(DBの戻り値)をテスト側で制御する
class StubDatabaseClient(DatabaseClient):
    def __init__(self, fetch_user_result: dict) -> None:
        self._fetch_user_result = fetch_user_result

    def fetch_user(self, user_id: str) -> dict:
        # user_id は今回は検証対象にしないので無視して固定値を返す
        return self._fetch_user_result


def test_load_display_name(monkeypatch):
    # 1) DatabaseClient を Stub に差し替える
    def _stub_ctor():
        # 本番コード内の `DatabaseClient()` 呼び出しがここに来る
        return StubDatabaseClient({"id": "42", "display_name": "テスト太郎"})

    # example.service モジュールが参照している DatabaseClient を差し替え
    monkeypatch.setattr(service, "DatabaseClient", _stub_ctor)

    # 2) テスト対象を実行
    act = service.load_display_name("42")

    # 3) 間接入力(スタブが返した display_name)が
    #    正しく処理されているかを検証
    assert act == "テスト太郎"

この例はMeszarosの定義の説明を容易にするために作られたもので、実際に間接入力を行う場合はよりスマートな方法を検討してください。

スタブ

Gerard Meszaros氏がテストスタブという分類を行う以前よりスタブという言葉はソフトウェアのテストの文脈で使用されていました。
「stub」は中英語の「stubbe」(木の切り株)から来ており、短く残ったものや不完全な残骸を意味します。コンピューターサイエンスでは、これが転じて「完全なコードの短いプレースホルダー」や「本物の処理を切断したような一時的な代用品」を指すようになったと思われます。
少なくとも 1979 年に出版された Glenford J. Myers のThe Art of Software Testingの初版では上位モジュールのテストのために呼び出し先の下位モジュールを模倣する「stub modules」という用語が用いられており、下位モジュールの代用品という意味でのスタブが既に登場しています。

テストスパイ

テストスパイは間接出力の観測ポイントとして機能するオブジェクトです。
テストスタブの機能に加えて、テスト対象からの依存コンポーネントの呼び出しを記録しておき、テストの検証時に、期待通りに依存コンポーネントを呼び出されたかを確認します。

以下はpythonのpytestによるテストスパイの例です。
この例では依存コンポーネントMessageClientを、SpyMessageClientに置き換えて、send_user_infoからの呼び出しを記録します。

依存コンポーネント example/external.py

class MessageClient:
    def send_user(self, user_dict: dict) -> bool:
        raise RuntimeError("real Message call!")

テスト対象 example/service.py

from .external import DatabaseClient, MessageClient

def send_user_info(user_id: str):
    db_client = DatabaseClient()
    msg_client = MessageClient()

    user_dict = db_client.fetch_user(user_id)
    return msg_client.send_user(user_dict)

テストコード

import pytest

from example import service
from example.external import DatabaseClient, MessageClient

class SpyMessageClient(MessageClient):
    def __init__(self) -> None:
        self.send_user_logs:list[dict] = []

    def send_user(self, user_dict: dict) -> bool:
        self.send_user_logs.append(user_dict)
        return True

def test_send_user_info_spy(monkeypatch):
    res_db_user = {"id": "42", "display_name": "テスト太郎"}
    # 1) DatabaseClient を Stub に差し替える
    def _stub_db_ctor():
        # 本番コード内の `DatabaseClient()` 呼び出しがここに来る
        return StubDatabaseClient(res_db_user)
    # 2) MessageClient を Spy に差し替える
    spy_msg_client = SpyMessageClient()
    def _spy_msg_ctor():
        # 本番コード内の `MessageClient()` 呼び出しがここに来る
        return spy_msg_client
    monkeypatch.setattr(service, "DatabaseClient", _stub_db_ctor)
    monkeypatch.setattr(service, "MessageClient", _spy_msg_ctor)

    act = service.send_user_info("42")
    assert act, "send_user_infoが正しく完了したこと"
    assert spy_msg_client.send_user_logs == [
        res_db_user
    ]

この例はMeszarosの定義の説明を容易にするために作られたもので、実際に間接出力の観測を行う場合はよりスマートな方法を検討してください。

スパイ性の違い

Gerard Meszarosの定義によるスパイは「関数の使用状況に関するデータを記録するスタブ」という定義でしたが、実際には異なる考え方の流派が存在しています。
たとえばmockitoやjestではSpyは「実装には干渉せずに、関数の使用状況に関するデータを記録する」という立場にたっています。

Test doubleの概念を意識しているTesting JavaScript Applicationsであっても以下のようにスパイの定義については後者の立場となっています。

3.3 Test doubles: Mocks, stubs, and spies
Mocks, stubs, and spies are objects used to modify and replace parts of your application to ease or enable testing. As a whole, they’re called test doubles.

  • Spies record data related to the usage of a function without interfering in its implementation.
  • Stubs record data associated with the usage of a function and change its behavior, either by providing an alternative implementation or return value.
  • Mocks change a function’s behavior, but instead of just recording information about its usage, they have expectations preprogrammed.

なお、mockitoやjestのスパイの挙動のデフォルトは「実装には干渉せずに、関数の使用状況に関するデータを記録する」動作ですが、そのスパイの挙動にスタブとしての間接入力を変更する機能も加えることができます。

モックオブジェクト

モックオブジェクトはテストスパイと同様に間接出力の観測ポイントとして機能するオブジェクトです。
ただし、モックオブジェクトはアサーションを使用して実際に受信した呼び出しを事前に定義された期待値と比較し、テストメソッドに代わってテストを失敗させるという点で、テストスパイとは異なります。
その結果、同じモックオブジェクトを使用するすべてのテストで、間接出力を検証するために採用されたロジックを再利用できます。

以下はpythonのpytestによるモックオブジェクトの例です。
この例では依存コンポーネントMessageClientを、MockMessageClientに置き換えて、send_user_infoからの呼び出しを検証します。

テストコード

class MockMessageClient(MessageClient):
    def __init__(self, expect_calls: list[dict]) -> None:
        self.expect_calls:list[dict] = expect_calls
        self.call_count = 0

    def send_user(self, user_dict: dict) -> bool:
        if self.call_count >= len(self.expect_calls):
            raise RuntimeError('期待より多く実行されました')
        assert self.expect_calls[self.call_count] == user_dict, "user_dictが期待通りの引数であること"
        self.call_count += 1
        return True
    
    def verify(self):
        assert self.call_count == len(self.expect_calls), "期待の数だけ呼ばれる"

def test_send_user_info_mock(monkeypatch):
    res_db_user = {"id": "42", "display_name": "テスト太郎"}
    # 1) DatabaseClient を Stub に差し替える
    def _stub_db_ctor():
        # 本番コード内の `DatabaseClient()` 呼び出しがここに来る
        return StubDatabaseClient(res_db_user)
    # 1) DatabaseClient を Stub に差し替える
    mock_msg_client = MockMessageClient([
        res_db_user
    ])
    def _mock_msg_ctor():
        return mock_msg_client
    monkeypatch.setattr(service, "DatabaseClient", _stub_db_ctor)
    monkeypatch.setattr(service, "MessageClient", _mock_msg_ctor)

    act = service.send_user_info("42")
    assert act, "send_user_infoが正しく完了したこと"
    mock_msg_client.verify()

この例はMeszarosの定義の説明を容易にするために作られたもので、実際に間接出力の検証を行う場合はよりスマートな方法を検討してください。

モックオブジェクトには基本的に2種類あります。

  • 厳密なモックオブジェクトは、正しい呼び出しが指定された順序とは異なる順序で受信された場合、テストに失敗します。
  • 寛容なモックオブジェクトは、順序が乱れた呼び出しを許容します。一部の寛容なモックオブジェクトは、予期しない呼び出しや失敗した呼び出しを許容または無視します。

今回の例では厳密なモックオブジェクトとなります。

モックオブジェクトの歴史

Gerard Meszaros氏がモックオブジェクトという分類を行う以前よりモックオブジェクトの概念は存在してました。

Steve Freeman, Nat Pryce著 実践テスト駆動開発によるとモックオブジェクトの歴史は1999年頃後半まで遡ります。
モックオブジェクトという手法は、Web向けコンテキスト広告システムを開発していたConnextra
の人たちによって発明されました

Tim Mackinnonらは、その結果を2001年にEndo‑Testing: Unit Testing with Mock Objectsとして発表します。

このペーパーではモックオブジェクトについて以下のようなものとしています。

We propose a technique called Mock Objects in which we replace domain code with dummy implementations that both emulate real functionality and enforce assertions about the behaviour of our code. These Mock Objects are passed to the target domain code which they test from inside, hence the term Endo-Testing. Unlike conventional techniques, however, assertions are not left in production code, but gathered together in unit tests.
私たちは Mock Objects と呼ぶ技法を提案する。これは、ドメインコードを、実際の機能をエミュレートすると同時に、コードの振る舞いに関するアサーションを強制するダミー実装に置き換えるものである。これらの Mock Object は、対象となるドメインコードに渡され、その内部からそれをテストする。このことから、Endo-Testing という用語を用いている。
しかしながら、従来の技法とは異なり、アサーションは本番コードの中に残されるのではなく、ユニットテスト内にまとめておかれる。

このペーパーの数年後、Tim MackinnonらはMock Roles, not Objectsを発表しました。ここではモックは単なる「テスト補助具」ではなく、オブジェクトの責務分割・インターフェイス設計を導くツールとして、テストで期待するメッセージ(role)を書き、それに合わせて実装を育てていくスタイルを具体例で示しています。

フェイクオブジェクト

フェイクオブジェクトは、依存コンポーネントの「動く代替実装」であり、テストスタブやモックオブジェクトのように間接入力・間接出力の制御や検証を目的とするものではありません。
本物のコンポーネントと同じインターフェイスを持ちながら、より簡易・高速に動作する“代替ミニ実装” をテスト中に利用するためのテストダブルです。

よくある例としてはデータベースアクセスを、より簡易なオンメモリ上のDBに置き換えたり、AWSへの実アクセスを行わずLocalStackで代替することが考えられます。

実世界でのスタブ、スパイ、モック

Gerard Meszarosが示した古典的なテストダブルの定義ではスタブ、スパイ、モックは明確に分類されていました。
しかし、現実世界で我々ソフトウェアエンジニアが、その定義を意識して単体テストを実装しているとは言えません。また、使用するテストダブルのライブラリもそれらのスタブ、スパイ、モックを明確に分けているわけでも、古典的なテストダブルの定義通りの機能を提供するわけでもありません。

2020年にテストダブルの使用状況を調査したペーパーAssessing Mock Classes: An Empirical Studyでは、モックオブジェクトとテストダブルという用語はしばしば同義的に使用されていると言及しています。

現在では、「モック」という単語が出てきた場合、それが何をさすかは文脈によって異なります。
ある状況ではGerard Meszarosの定義である事前に定義した期待する振る舞いを検証するスタブを想定しています。
ある状況では、本物のオブジェクトの一部メソッドだけを置き換えた「部分モック(partial mock)」を指している場合もあります。
ある状況では、事前に期待する振る舞いを定義せずに事後に期待する振る舞いを検証するテストスパイのようなものを指している場合があります。
ある状況ではそれは依存コンポーネントを置き換えるテストダブル全体を指す意味で使われることがあります。

それは「モック」だけでなく、「スパイ」という言葉についても同じです。
Gerard Meszarosの定義では使用状況を記録するスタブです。しかし、現在では「使用状況を記録できる部分モック(partial mock)」を「スパイ」と呼称する場合があります。

ここでは、テストダブルのライブラリを個別具体的にみて、スタブ、スパイ、モックをどのように使用していて、どう呼称しているかを調べます。

Javaを対象としたテストダブル

Javaを対象としたテストダブルの特徴として、テストダブルの置き換えは依存注入が好まれる傾向があります。

たとえばテスト対象クラスは以下のように依存するコンポーネントのインターフェイスを定義しておき、なんらかの手段で実態を差し替え可能にしておくことが好まれます。

Javaのテスト対象の例

依存コンポーネントのインターフェイス

public interface DatabaseClient {
    Map<String, String> fetchUser(String userId);
}
public interface MessageClient {
    boolean sendUser(Map<String, String> userDict);
}

依存コンポーネントの実態

public class RealDatabaseClient implements DatabaseClient {
    @Override
    public Map<String, String> fetchUser(String userId) {
        // 本来は DB アクセス
        throw new RuntimeException("real DB call!");
    }
}
public class RealMessageClient implements MessageClient {
    public String getVersion() {
        return "ver0.1";
    }
    @Override
    public boolean sendUser(Map<String, String> userDict) {
        // 本来は外部サービス呼び出し
        throw new RuntimeException("real message call!");
    }
}

テスト対象

public class UserService {

    private final DatabaseClient databaseClient;
    private final MessageClient messageClient;

    public UserService(DatabaseClient databaseClient, MessageClient messageClient) {
        this.databaseClient = databaseClient;
        this.messageClient = messageClient;
    }

    public boolean sendUserInfo(String userId) {
        Map<String, String> userDict = databaseClient.fetchUser(userId);
        return messageClient.sendUser(userDict);
    }
}

しかしながら、レガシーコードの対応など、テスト対象を修正できないケースは存在するため、いくつかのライブラリはバイトコードを改変するなどをして、依存注入なしでの置き換えを実現しているものも存在します。

Easy Mock

EasyMockは2001年頃から存在するJava用のテストダブルのライブラリです。2025年にv5.6.0がリリースされています。

Gerard Meszarosの想定していたモックオブジェクトの挙動に近しい挙動をします。

EasyMockをしようしたテスト手順は以下のようになります。

  1. モックを作成する
  2. そのモックをテスト対象のクラスに設定する(依存の注入)
  3. モックに「どのように振る舞ってほしいか」を記録する
  4. すべてのモックに「これから実際のテストを行う」と知らせる
  5. テストを実行する
  6. 想定していた呼び出しがすべて行われたことを確認する(verify)

具体的にDatabaseClientとMessageClientのインターフェイスをモックオブジェクトに置き換えてテストする例は以下の通りです。

    @Test
    void test_sendUserInfo_with_mocks() {
        // arrange
        DatabaseClient dbMock = createMock(DatabaseClient.class);
        MessageClient msgMock = createMock(MessageClient.class);

        Map<String, String> dbUser = Map.of(
                "id", "42",
                "display_name", "テスト太郎"
        );

        expect(dbMock.fetchUser("42")).andReturn(dbUser);
        expect(msgMock.sendUser(dbUser)).andReturn(true);

        replay(dbMock, msgMock);

        UserService sut = new UserService(dbMock, msgMock);

        // act
        boolean act = sut.sendUserInfo("42");

        // assert
        assertTrue(act);

        // 期待通りに呼ばれていれば verify() が通る(回数も含めてチェックされる)
        verify(dbMock, msgMock);
    }

もともと、EasyMockはインターフェイスにたいしてしかモックオブジェクトの作成はできませんでしたが、現在のバージョンではクラスのモックも可能になっています

        RealDatabaseClient dbMock = createMock(RealDatabaseClient.class);
        RealMessageClient msgMock = createMock(RealMessageClient.class);

EasyMockにはスタブ専用の機能はありません。Nice Mocksを使用することで呼び出しの厳密性を検証せずに間接入力の制御のみを行うスタブ的な役割を行えます。

        // ★ DatabaseClient は「スタブ的」に扱う:戻り値だけ決めて、回数は気にしない
        DatabaseClient dbStub = createNiceMock(DatabaseClient.class);
        // 略
        // 「42 が来たらこのデータを返す」だけ約束して、回数制約はゆるめる
        expect(dbStub.fetchUser("42"))
                .andReturn(dbUser)
                .anyTimes();   // ← スタブっぽく、何回呼ばれても OK にする
        // 略
        verify(msgMock);

partialMockBuilderを用いて、部分モックも可能です。以下の例ではsendUserはモックオブジェクトの結果を返しますが、たとえばgetVersion()などの別のメソッドでは、実際のメソッドが実行されてその結果を返します。

        // ★ User Guide 推奨の partialMockBuilder を使用
        RealMessageClient msgPartialMock = partialMockBuilder(RealMessageClient.class)
                .addMockedMethod("sendUser")  // このメソッドだけモック
                .createMock();

        expect(msgPartialMock.sendUser(dbUser)).andReturn(true);

EasyMockではとりあえず動かして、あとから呼び出しを検証するようなテストスパイのような機能は提供していません。

EasyMockでは置き換えられるメソッドに制限があります。たとえばprivateメソッドなどは置き換えができません。
この制限を回避するためアドオンとしてPowerMockを使用する手段があります。
しかしながら2020年以降、PowerMockのリリースがされておらず、作者自身が「PowerMockをメンテナンスする時間がない」と言っているので、今後使用できる手段であるかは慎重に検討する必要があります。

JMock2

jMock2は2007年頃から存在するテストダブルのライブラリです。2024年に2.13.1がリリースされています。

jMock自体はTim MackinnonらのペーパーMock Roles, not Objectsで存在を確認できます。
jMock2の存在を確認できるもっとも有名な書籍の一つはSteve Freeman, Nat Pryce著 実践テスト駆動開発です。

JMock2のデフォルトではインターフェイスを対象にしてのみモックオブジェクトを作成できます。

package example.service;

import example.external.DatabaseClient;
import example.external.MessageClient;
import org.jmock.Expectations;
import org.jmock.junit5.JUnit5Mockery;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertTrue;

class UserServiceJMockTest {

    // JUnit 5 での jMock 連携:@RegisterExtension + JUnit5Mockery
    @RegisterExtension
    JUnit5Mockery context = new JUnit5Mockery();

    @Test
    void test_sendUserInfo_with_jmock() {
        // --- mock の生成 ---
        DatabaseClient db = context.mock(DatabaseClient.class);
        MessageClient msg = context.mock(MessageClient.class);

        Map<String, String> dbUser = Map.of(
                "id", "42",
                "display_name", "テスト太郎"
        );

        // --- 期待の定義(jMock DSL) ---
        context.checking(new Expectations() {{
            // DatabaseClient.fetchUser("42") が 1 回呼ばれ、dbUser を返す
            oneOf(db).fetchUser("42");
            will(returnValue(dbUser));

            // MessageClient.sendUser(dbUser) が 1 回呼ばれ、true を返す
            oneOf(msg).sendUser(dbUser);
            will(returnValue(true));
        }});

        UserService sut = new UserService(db, msg);

        // --- act ---
        boolean act = sut.sendUserInfo("42");

        // --- assert ---
        assertTrue(act);

        // 期待が満たされているかは、テスト終了時に JUnit5Mockery が自動で検証してくれる。
        // 明示的に書きたい場合は:
        context.assertIsSatisfied();
    }
}

jMock2はスタブを表す機能はありませんが、allowingまたはatLeast(0)を使用することでスタブのような振る舞いをするモックオブジェクトを作れます。

        // --- 期待の定義(jMock DSL) ---
        context.checking(new Expectations() {{
            // DatabaseClient.fetchUser("42") が0回でもよいし、何回呼ばれてもよい。
            // 呼ばれたらdbUser を返す
            allowing(db).fetchUser("42");
            will(returnValue(dbUser));
        }});
        // --- 期待の定義(jMock DSL) ---
        context.checking(new Expectations() {{
            // DatabaseClient.fetchUser("42") が 0 回以上呼ばれ、dbUser を返す
            atLeast(0).of(db).fetchUser("42");
            will(returnValue(dbUser));
        }});

JMock2にはスパイや部分モックの機能については確認できません

jmock-impostersアドオンを使用することで実クラスにたいしてモックオブジェクトを作成することができます。

package example.service;
import example.external.RealDatabaseClient;
import example.external.RealMessageClient;
import org.jmock.Expectations;
import org.jmock.junit5.JUnit5Mockery;
import org.jmock.imposters.ByteBuddyClassImposteriser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertTrue;

class UserServiceJMockTestRealClass {

    // JUnit 5 での jMock 連携:@RegisterExtension + JUnit5Mockery
    @RegisterExtension
    JUnit5Mockery context = new JUnit5Mockery() {{
        // クラスもモックできるようにする
        setImposteriser(ByteBuddyClassImposteriser.INSTANCE);
    }};


    @Test
    void test_sendUserInfo_with_jmock() {
        // --- mock の生成 ---
        RealDatabaseClient db = context.mock(RealDatabaseClient.class);
        RealMessageClient msg = context.mock(RealMessageClient.class);

        Map<String, String> dbUser = Map.of(
                "id", "42",
                "display_name", "テスト太郎"
        );

        // --- 期待の定義(jMock DSL) ---
        context.checking(new Expectations() {{
            // DatabaseClient.fetchUser("42") が 1 回呼ばれ、dbUser を返す
            oneOf(db).fetchUser("42");
            will(returnValue(dbUser));

            // MessageClient.sendUser(dbUser) が 1 回呼ばれ、true を返す
            oneOf(msg).sendUser(dbUser);
            will(returnValue(true));
        }});

        UserService sut = new UserService(db, msg);

        // --- act ---
        boolean act = sut.sendUserInfo("42");

        // --- assert ---
        assertTrue(act);

        // 期待が満たされているかは、テスト終了時に JUnit5Mockery が自動で検証してくれる。
        // 明示的に書きたい場合は:
        context.assertIsSatisfied();
    }
}

参考

Mockito

mockitoは2008 年初に登場したテストダブルのフレームワークで、2025年にv5.20.0をリリースしています。
mockitoはEasyMockをベースに既存の改善することからはじまりました
EasyMockなどの既存のモックオブジェクトツールにあった実行前に検証関連のコードを記述することをなしにシンプルさを目指しています。

mockitoという名前に反して、Test Spyのフレームワークといったほうが近いです。実際、mockitoの作者も「Mockito mock」は「Test Spy」とよぶべきだったといっています。

以下はmockitoでのインターフェイスのモックアップのサンプルになります。

package example.service;

import example.external.RealDatabaseClient;
import example.external.RealMessageClient;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.*;

class UserServiceMockitoStyleTest {
    @Test
    void test_sendUserInfo_with_if_mocks() {
        // arrange
        DatabaseClient dbMock = mock(DatabaseClient.class);
        MessageClient msgMock = mock(MessageClient.class);

        Map<String, String> dbUser = Map.of(
                "id", "42",
                "display_name", "テスト太郎"
        );

        when(dbMock.fetchUser("42")).thenReturn(dbUser);
        when(msgMock.sendUser(dbUser)).thenReturn(true);

        UserService sut = new UserService(dbMock, msgMock);

        // act
        boolean act = sut.sendUserInfo("42");

        // assert
        assertTrue(act);
        // Mockito 流 Mock: verify で「何回・どんな引数で呼ばれたか」を検証
        verify(dbMock, times(1)).fetchUser("42");
        verify(msgMock, times(1)).sendUser(dbUser);
        verifyNoMoreInteractions(dbMock, msgMock);
    }
}

EasyMockなどにくらべてテスト実行前の設定がシンプルになっています。
mockitoはクラスにたいしてもモックオブジェクトを作成できます。

        RealDatabaseClient dbMock = mock(RealDatabaseClient.class);
        RealMessageClient msgMock = mock(RealMessageClient.class);

mockitoにはstubというクラスやメソッドはありませんが、mockを使用してスタブの役割を果たすことが可能です。これは前述のEasyMockやJMock2よりシンプルな設定です。

mockitoにはspyという名前の機能も存在します
この機能は以下のような機能です。

  • 実クラスをラップする
    • 必要なメソッドだけ置き換えて部分的なモックとしてつかえる
    • 実メソッドを呼びつつ、その呼び出しを検証できる

mockitoのspyの使用方法については下記を参照してください。

    @Test
    void test_sendUserInfo_with_spy1() {
        // Mockito の spy(...) で部分モックを作る
        RealMessageClient realRealMessageClient = new RealMessageClient();
        RealMessageClient spyMsg = spy(realRealMessageClient);
        // 何も設定しなければ本物の関数を読む
        assertEquals(spyMsg.getVersion(), "ver0.1");
        verify(spyMsg, times(1)).getVersion();

        // 特定のメソッドのみスタブ化(部分モック)
        doReturn("xxxx").when(spyMsg).getVersion();
        assertEquals(spyMsg.getVersion(), "xxxx");
        verify(spyMsg, times(2)).getVersion();
    }

このようにmockitoでのspyの定義はGerard Meszarosの定義とはズレがあります。
Gerard Meszarosの定義では使用状況を記録するスタブです。しかし、mockitoでは「使用状況を記録する部分モック(partial mock)」の機能を「spy」という関数で提供しています。このことは言及はされていますが、修正の予定はありません。

mockitoは依存性の注入なしでのモックを可能としており、mockConstructionを使用することで以下のようにテスト対象コードが依存するクラス DatabaseClientMessageClientを置き換えます。

テスト対象の例

public class UserServiceNoDI {
    public boolean sendUserInfo(String userId) {
        RealDatabaseClient databaseClient = new RealDatabaseClient();
        RealMessageClient messageClient = new RealMessageClient();

        Map<String, String> userDict = databaseClient.fetchUser(userId);
        return messageClient.sendUser(userDict);
    }
}

テストコード

package example.service;

import example.external.DatabaseClient;
import example.external.MessageClient;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

/**
 * DI していない UserService(内部で new している)のテスト。
 * Mockito の mockConstruction(...) を使ってコンストラクタをモックする。
 */
class UserServiceNoDiMockitoTest {
    /**
     * sendUserInfo() のテスト。
     * - new DatabaseClient() と new MessageClient() の両方を mockConstruction でフック
     * - DatabaseClient#fetchUser の戻り値をスタブ
     * - MessageClient#sendUser の引数・呼び出し回数を verify
     */
    @Test
    void test_sendUserInfo_with_constructor_mocks() {
        Map<String, String> dbUser = Map.of(
                "id", "42",
                "display_name", "テスト太郎"
        );

        // RealDatabaseClient のコンストラクタをモック
        try (MockedConstruction<RealDatabaseClient> dbConstr = mockConstruction(
                     RealDatabaseClient.class,
                     (mock, context) -> when(mock.fetchUser("42")).thenReturn(dbUser)
             );
             // RealMessageClient のコンストラクタもモック
             MockedConstruction<RealMessageClient> msgConstr = mockConstruction(
                     RealMessageClient.class,
                     (mock, context) -> when(mock.sendUser(dbUser)).thenReturn(true)
             )) {

            UserServiceNoDI sut = new UserServiceNoDI();

            boolean act = sut.sendUserInfo("42");

            assertTrue(act, "sendUserInfo が正常終了すること");

            // new RealDatabaseClient(), new RealMessageClient() が 1 回ずつ呼ばれたこと
            assertEquals(1, dbConstr.constructed().size(), "RealDatabaseClient が 1 回だけ new されること");
            assertEquals(1, msgConstr.constructed().size(), "RealMessageClient が 1 回だけ new されること");

            RealDatabaseClient dbMock = dbConstr.constructed().get(0);
            RealMessageClient msgMock = msgConstr.constructed().get(0);

            // Mockito 流 Mock として verify
            verify(dbMock, times(1)).fetchUser("42");
            verify(msgMock, times(1)).sendUser(dbUser);
            verifyNoMoreInteractions(dbMock, msgMock);
        }
    }
}

参考:

C#(.NET)を対象としたテストダブル

C#はJavaとよく似た言語特性を有しているため、依存性の注入をなしにテストダブルを使用するのが難しい傾向があります。

C#のテスト対象の例

依存コンポーネントのインターフェイス

// Example/External/IDatabaseClient.cs
using System.Collections.Generic;

namespace Example.External
{
    public interface IDatabaseClient
    {
        Dictionary<string, string> FetchUser(string userId);
    }
}
// Example/External/IMessageClient.cs
using System.Collections.Generic;

namespace Example.External
{
    public interface IMessageClient
    {
        bool SendUser(Dictionary<string, string> userDict);
    }
}
// Example/External/RealDatabaseClient.cs
using System;
using System.Collections.Generic;

namespace Example.External
{
    // 本来は DB に接続する実装だが、ここでは例として NotImplemented
    public class RealDatabaseClient : IDatabaseClient
    {
        public virtual Dictionary<string, string> FetchUser(string userId)
        {
            throw new NotImplementedException("Real DB access is not implemented.");
        }
    }
}
// Example/External/RealMessageClient.cs
using System;
using System.Collections.Generic;

namespace Example.External
{
    public class RealMessageClient : IMessageClient
    {
        // モックしない限りは例外を投げるだけの実装
        public virtual bool SendUser(Dictionary<string, string> userDict)
        {
            throw new NotImplementedException("Real message sending is not implemented.");
        }

        // EasyMock 側で getVersion() を検証していたのに対応
        public virtual string GetVersion() {
          return "ver0.1";
        }
    }
}

テスト対象

// Example/Service/UserService.cs
using System.Collections.Generic;
using Example.External;

namespace Example.Service
{
    public class UserService
    {
        private readonly IDatabaseClient _databaseClient;
        private readonly IMessageClient _messageClient;

        public UserService(IDatabaseClient databaseClient, IMessageClient messageClient)
        {
            _databaseClient = databaseClient;
            _messageClient = messageClient;
        }

        public string LoadDisplayName(string userId)
        {
            Dictionary<string, string> row = _databaseClient.FetchUser(userId);
            return row["display_name"];
        }

        public bool SendUserInfo(string userId)
        {
            Dictionary<string, string> userDict = _databaseClient.FetchUser(userId);
            return _messageClient.SendUser(userDict);
        }
    }
}

Moq

Moqは2000年代後半から存在する.NETのモックライブラリです。2024年にv4.20.72がリリースされています。

置き換え可能なクラスには厳しい制限が付いています。任意のインターフェース タイプをモックに使用できますが、クラスの場合、モックできるのは抽象メンバーと仮想メンバーのみです。
また、依存の注入なしで依存コンポーネントをテストダブルに置き換える機能は存在しません。

Moqを利用したテストの例は以下の通りです。

using System.Collections.Generic;
using Example.External;
using Example.Service;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Example.Tests.Service
{
    [TestClass]
    public class UserServiceMoqStyleTests
    {
        [TestMethod]
        public void SendUserInfo_with_if_mocks()
        {
            // arrange
            var dbMock = new Mock<IDatabaseClient>(MockBehavior.Strict);
            var msgMock = new Mock<IMessageClient>(MockBehavior.Strict);

            var dbUser = new Dictionary<string, string>
            {
                ["id"] = "42",
                ["display_name"] = "テスト太郎",
            };

            dbMock
                .Setup(x => x.FetchUser("42"))
                .Returns(dbUser)
                .Verifiable();

            msgMock
                .Setup(x => x.SendUser(dbUser))
                .Returns(true)
                .Verifiable();

            var sut = new UserService(dbMock.Object, msgMock.Object);

            // act
            bool act = sut.SendUserInfo("42");

            // assert
            Assert.IsTrue(act);
            dbMock.Verify();
            msgMock.Verify();
        }
    }
}

上記の例はいわゆるGerard Meszarosが想定したモックオブジェクトで、事前に定義した期待する振る舞いを検証するスタブになります。
MockBehavior.Strict/Loose に関係なく Moq の Mock は呼び出しを Invocations プロパティに記録します。
ただし Strict モードでは、未設定の呼び出しで例外が出るため、Spy 的に「とりあえず呼ばせておいて後から Invocations を読む」使い方には Loose の方が扱いやすい、という意味でここでは Loose を使っています。

        [TestMethod]
        public void SendUserInfo_with_spy_like_message_client()
        {
            // arrange
            var dbStub = new Mock<IDatabaseClient>();
            var dbUser = new Dictionary<string, string>
            {
                ["id"] = "42",
                ["display_name"] = "テスト太郎",
            };

            dbStub
                .Setup(x => x.FetchUser("42"))
                .Returns(dbUser);

            var msgMock = new Mock<IMessageClient>(); // MockBehavior.Loose

            // 戻り値だけスタブしておく(Spy 的にはここは「スタブの部分」)
            msgMock
                .Setup(m => m.SendUser(It.IsAny<Dictionary<string, string>>()))
                .Returns(true);

            var sut = new UserService(dbStub.Object, msgMock.Object);

            // act
            bool act = sut.SendUserInfo("42");

            // assert
            Assert.IsTrue(act);

            // 「Spy 的な事後検証」パート
            var sendUserInvocations = msgMock.Invocations
                .Where(inv => inv.Method.Name == nameof(IMessageClient.SendUser))
                .ToList();

            // 呼び出し回数の検証
            Assert.AreEqual(1, sendUserInvocations.Count, "SendUser が 1 回だけ呼ばれていること");

            // 引数の検証
            var firstCall = sendUserInvocations[0];
            var arg0 = (Dictionary<string, string>)firstCall.Arguments[0];

            // 同じ Dictionary が渡っているか(参照一致で確認)
            Assert.AreSame(dbUser, arg0);
            Assert.AreEqual("42", arg0["id"]);
            Assert.AreEqual("テスト太郎", arg0["display_name"]);
        }

CallBase=Trueを使用することで部分モックは可能となっています。

            // RealMessageClient の部分モック
            var msgPartialMock = new Mock<RealMessageClient>(MockBehavior.Loose)
            {
                // これを true にすると、Setup していないメソッドは
                // 実クラスの実装(base)を呼び出す
                CallBase = true
            };
            // sendUser だけモック(EasyMock の addMockedMethod("sendUser") 相当)
            msgPartialMock
                .Setup(m => m.SendUser(dbUser))
                .Returns(true)
                .Verifiable();

            // 略
            // CallBase = true にしているので Version は実装がそのまま呼ばれる
            Assert.AreEqual("ver0.1", msgPartialMock.Object.GetVersion());

これらのInvocationsやCallBaseを使用してもGerard Meszarosが想定したスパイや、部分モックはおこなえますが、mockitoのspyのような実関数の呼び出し方も記録するような手段はありません。

Moqの設計思想としてはREADMEにあるようにモック、スタブ、フェイクの違いを意識せずとも使えるような思想になっています。

参考

Microsoft Fakes

Microsoft Fakesは.NETの依存コンポーネントをテストダブルに置き換える強力な手段の一つです。
StubとShimの2通りの方法が存在します。

  • Stub:インターフェイスや仮想メソッドを、テスト用の「代替クラス」に差し替える仕組み
  • Shim:コンパイル済みコードへの呼び出し(static/非virtualなど)を、実行時に横取りして別の処理に差し替える仕組み。

特にShimの機能は強力で、依存の注入なしで、あらゆるメソッドをテストダブルに置き換えることが可能なためレガシーな環境において有効な手段の一つになります。
ただ、MicrosoftFakesは置き換える仕組みであり、モックやスパイの機能を提供しているわけではないので、それは自分で対応する必要があります。

Microsoft Fakesがどのように動作するかについては下記を参照してください。

Microsoft Fakesは強力なテストダブルを実現するツールですが、Visual Studio Enterpriseでのみ使用が可能です。
OSSで類似のプロジェクトはいくつかありますが、更新頻度に不安があるものがあります。

現時点でOSSのMicrosoft Fakesの代替を検討するならposeの後継である、poserの採用を検討してください。

以下に簡単な説明を書きました。
https://zenn.dev/mima_ita/articles/463bb8e5c8b9f1

Pythonを対象としたテストダブル

Pythonの場合、その言語特性からか、依存の注入なしでテストダブルに置き換えることが容易になっています。
たとえば、pytestのmonkeypatchを確認すると、シンプルな実装で関数を置き換えていることが確認できます。

テスト対象のコード

依存オブジェクト example/external.py

class DatabaseClient:
    def fetch_user(self, user_id: str) -> dict:
        # 本来はDBアクセス
        raise RuntimeError("real DB call!")

class MessageClient:
    def send_user(self, user_dict: dict) -> bool:
        raise RuntimeError("real DB call!")
    def get_version(self):
        return "v1.0"

テスト対象 example/service.py

from .external import DatabaseClient, MessageClient


def send_user_info(user_id: str):
    db_client = DatabaseClient()
    msg_client = MessageClient()

    user_dict = db_client.fetch_user(user_id)
    return msg_client.send_user(user_dict)

def send_user_info_di(user_id: str, db_client, msg_client):
    user_dict = db_client.fetch_user(user_id)
    return msg_client.send_user(user_dict)

unittest.mock

unittest.mockはPythonの標準ライブラリに含まれるテストダブルのライブラリです。Michael Foord が作ったサードパーティライブラリ mock がPython 3.3〜標準ライブラリに取り込まれたものになります。

Meszarosが想定していたEasyMockのような期待する振る舞いを登録してからテストを実行するという動きではなく、テストを実行し呼ばれ方を検証するという設計思想になっています。(mockitoの思想に近しい)

以下のような依存を注入する関数をテストする場合はMockクラスを使用します。

テストコード

def test_send_user_info_di():
    res_db_user = {"id": "42", "display_name": "テスト太郎"}
    mock_db = Mock(spec=DatabaseClient)
    mock_msg = Mock(spec=MessageClient)

    mock_db.fetch_user.return_value = res_db_user
    mock_msg.send_user.return_value = res_db_user

    act = service.send_user_info_di("42", mock_db, mock_msg)
    assert act, "send_user_infoが正しく完了したこと"

    mock_db.fetch_user.assert_called_once_with("42")
    mock_msg.send_user.assert_called_once_with(res_db_user)

依存を注入しない関数をテストする場合は、patchを使用します。
with patch.object(...): の コンテキストのスコープ内に限り、元の実装ではなく、テストダブルに差し変わります。

テストコード

def test_send_user_info_mock_instance():
    res_db_user = {"id": "42", "display_name": "テスト太郎"}

    with patch("example.service.DatabaseClient") as MockDatabaseClient, \
        patch("example.service.MessageClient") as MockMessageClient:
        mock_db = MockDatabaseClient.return_value
        mock_db.fetch_user.return_value = res_db_user
        mock_msg = MockMessageClient.return_value
        mock_msg.send_user.return_value = True

        act = service.send_user_info("42")

        assert act, "send_user_infoが正しく完了したこと"

        # 「モック」として send_user が期待通り 1回だけ呼ばれたことを厳密に検証
        mock_msg.send_user.assert_called_once_with(res_db_user)

部分モックの方法はいくつかあります。
Mock(wraps=元オブジェクト,)を使用する方法ではMockのwraps引数に元になるオブジェクトを指定します。

def test_like_partial_mock1():
    res_db_user = {"id": "42", "display_name": "テスト太郎"}
    mock_db = Mock(spec=DatabaseClient)
    real_msg = MessageClient()
    mock_msg = Mock(wraps=real_msg, spec=MessageClient)

    mock_db.fetch_user.return_value = res_db_user
    mock_msg.send_user.return_value = res_db_user

    act = service.send_user_info_di("42", mock_db, mock_msg)
    assert act, "send_user_infoが正しく完了したこと"

    mock_db.fetch_user.assert_called_once_with("42")
    mock_msg.send_user.assert_called_once_with(res_db_user)
    print(mock_msg.get_version()) # 元関数が呼ばれる
    print(mock_msg.get_version.call_count) # 1

patch使用時に置き換えたいメソッドだけを指定することでも、部分モックができます。

def test_send_user_info_mock_mock_lib():
    res_db_user = {"id": "42", "display_name": "テスト太郎"}

    with patch.object(
            DatabaseClient, "fetch_user",
            return_value=res_db_user
        ), \
        patch.object(
            MessageClient, "send_user",
            return_value=True
        ) as mock_send_user:
        # テスト対象を実行
        act = service.send_user_info("42")

        assert act, "send_user_infoが正しく完了したこと"

        # 「モック」として send_user が期待通り 1回だけ呼ばれたことを厳密に検証
        mock_send_user.assert_called_once_with(res_db_user)
        print(MessageClient().get_version())

部分モックした場合、戻り値をしていない他のメソッドは本物の関数を呼び出せるが、call_countなどの呼び出し方法の記録も行われています。
この挙動はmockitoのspyに類似しています。

参考

JavaScriptを対象としたテストダブル

JavaScriptの場合、その言語特性からか、依存の注入なしでテストダブルに置き換えることが容易になっています。

以下のページにあるように、JavaScriptのテストにおけるテストダブル の概念全体が「モック」としてあつかわれる傾向が多いです。
[AskJS] Can we talk about Stubs, Spies and Mocks in JavaScript and what a mess they are?

テスト対象

依存コンポーネント src/external.js

class DatabaseClient {
  fetchUser(userId) {
    // 本来は DB アクセス
    throw new Error("real DB call!");
  }
}

class MessageClient {
  sendUser(userDict) {
    console.log("本物", userDict);
  }
  getVersion() {
    return "v0.1";
  }
}

module.exports = {
  DatabaseClient,
  MessageClient,
};

テスト対象 src/service.js

const { DatabaseClient, MessageClient } = require("./external");

function sendUserInfo(userId) {
  const dbClient = new DatabaseClient();
  const msgClient = new MessageClient();
  const userDict = dbClient.fetchUser(userId);
  return msgClient.sendUser(userDict);
}

function sendUserInfoDi(userId, dbClient, msgClient) {
  const userDict = dbClient.fetchUser(userId);
  return msgClient.sendUser(userDict);
}

module.exports = {
  sendUserInfo,
  sendUserInfoDi
};

SinonJS

SinonJSは2010年頃から公開されているテストダブルのライブラリです。

stub機能は間接入力を制御する機能とともに、使用状況を記録します。
Gerard Meszarosの想定するテストスパイに該当します。

const external = require("../src/external");
const service = require("../src/service");
const sinon = require('sinon')

afterEach(() => {
  sinon.restore();
});


test("スタブの例ー依存関係注入", () => {
  const resDbUser = { id: "42", display_name: "テスト太郎" };
  const dbStub = {
    fetchUser: sinon.stub().returns(resDbUser),
  };
  const msgStub = {
    sendUser: sinon.stub().returns(true)
  };
  const act = service.sendUserInfoDi("42", dbStub, msgStub);

  expect(act).toBe(true);

  // 「どう呼ばれたか」を検証(Spyのような役割)
  expect(dbStub.fetchUser.callCount).toBe(1);
  expect(dbStub.fetchUser.calledWith("42")).toBeTruthy();
  expect(msgStub.sendUser.callCount).toBe(1);
  expect(msgStub.sendUser.calledWith(resDbUser)).toBeTruthy();
});

spyは本物の関数に対する使用状況を記録するための機能です。

test("spyの例", () => {
  const resDbUser = { id: "42", display_name: "テスト太郎" };
  const msgObj = new external.MessageClient()
  const msgSendUserSpy = sinon.spy(msgObj, "sendUser");
  const dbStub = {
    fetchUser: sinon.stub().returns(resDbUser),
  };
  const act = service.sendUserInfoDi("42", dbStub, msgObj);

  // 「どう呼ばれたか」を検証(Spyのような役割)
  expect(dbStub.fetchUser.callCount).toBe(1);
  expect(dbStub.fetchUser.calledWith("42")).toBeTruthy();
  expect(msgSendUserSpy.callCount).toBe(1);
  expect(msgSendUserSpy.calledWith(resDbUser)).toBeTruthy();

});

mockは事前に期待する振る舞いを定義する必要のあるGerard Meszarosの想定するモックオブジェクトになっています。

test("モックの例", () => {
  const resDbUser = { id: "42", display_name: "テスト太郎" };
  const msgObj = new external.MessageClient()
  const msgMock = sinon.mock(msgObj);
  msgMock
      .expects("sendUser")
      .once()
      .withArgs(resDbUser)
      .returns(true);
  const dbStub = {
    fetchUser: sinon.stub().returns(resDbUser),
  };
  const act = service.sendUserInfoDi("42", dbStub, msgObj);

  expect(act).toBe(true);

  // 「どう呼ばれたか」を検証(Spyのような役割)
  expect(dbStub.fetchUser.callCount).toBe(1);
  expect(dbStub.fetchUser.calledWith("42")).toBeTruthy();
  msgMock.verify();

});

fakeは共通のテストダブルを作り、あとからそれを差し替えるための機能です。これはGerard Meszarosの考えるフェイクオブジェクトとは異なる概念になります。

SinonJSでのfakeの例は、以下のようにloggerのinfo, warn, errorに同じfakeに置き換えるような使いかたが考えられます。

const logFake = sinon.fake();  // 呼び出し履歴だけ欲しい

sinon.replace(logger, "info", logFake);
sinon.replace(logger, "warn", logFake);
sinon.replace(logger, "error", logFake);

// どのレベル経由で呼ばれたかも fake.getCalls() から分かる

Jest

JestはFacebookが2011年から開発したテストフレームワークです。2014年にオープンソース化され、2022年にはOpenJS Foundationに移管されています。2025年にはv30.0がリリースされています。

Jestはテストダブルの機能をサポートしており、その設計思想はmockitoやunittest.mockに近いものがあり、モックオブジェクトに対して事前の期待する振る舞いの設定を不要としています。
Testing JavaScript ApplicationsではJestのテストダブルの機能について3.3 Test doubles: Mocks, stubs, and spiesで以下のように言及しています。

NOTE Engineers often conflate the terms mocks, stubs, and spies, even though, formally, these terms have different definitions.
Especially in the context of Jest, you will frequently see people referring to stubs and spies as mocks. This confusion happens because Jest’s API and documentation tends to use the name mock for every kind of test double.
Ironically, if we adhere to the most accepted definition of a mock, it’s the only kind of test double that Jest does not include.
Note: エンジニアは、形式的にはそれぞれ異なる定義を持つ用語であるにもかかわらず、モック、スタブ、スパイという用語を混同して使ってしまうことがよくあります。
特に Jest の文脈では、スタブやスパイを「モック」と呼んでいるのを頻繁に目にします。
この混乱が起きるのは、Jest の API やドキュメントが、あらゆる種類のテストダブルに対して mock という名前を使う傾向があるからです。
皮肉なことに、もし最も広く受け入れられている「モック」の定義に従うなら、Jest に含まれていない唯一のテストダブルこそが、そのモックなのです。

Jestがあらゆる種類のテストダブルに対してモックを使う事例はMock Functionsで確認できます。

Mock functions are also known as "spies", because they let you spy on the behavior of a function that is called indirectly by some other code, rather than only testing the output.
モック関数は「スパイ(spy)」とも呼ばれます。なぜなら、単に出力だけをテストするのではなく、ほかのコードから間接的に呼び出される関数の振る舞いを「監視(スパイ)」できるからです。

このことから、Jestの世界では「モック」は依存コンポーネントを置き換える「テストダブル」全体を指す意味で使われていることがわかります。

jest.fn()で作成したオブジェクトは、呼び出しの記録とスタブのような役割をするいわゆるテストスパイとして使用できます。

const { DatabaseClient, MessageClient } = require("../src/external");
const service = require("../src/service");

afterEach(() => {
  jest.restoreAllMocks(); // spyOn を元に戻す
});
test("作成したオブジェクトを渡す", () => {
  const resDbUser = { id: "42", display_name: "テスト太郎" };
  const mockDb = {
    fetchUser: jest.fn().mockReturnValue(resDbUser),
  };
  const mockMsg = {
    sendUser: jest.fn().mockReturnValue(true),
  };
  const act = service.sendUserInfoDi("42", mockDb, mockMsg);
  expect(act).toBe(true);

  // 「どう呼ばれたか」を検証(Spy)
  expect(mockDb.fetchUser).toHaveBeenCalledTimes(1);
  expect(mockDb.fetchUser).toHaveBeenCalledWith("42");

  expect(mockMsg.sendUser).toHaveBeenCalledTimes(1);
  expect(mockMsg.sendUser).toHaveBeenCalledWith(resDbUser);
});

依存の注入が行えないテスト対象は、以下の方法があります。

  • prototype中のメソッドをspyOnで差し替える
  • jest.mockを使用してクラス自体をjest.fnとする

prototypeの置き換え例

test("spyOnでの部分的な差し替え", () => {
  const resDbUser = { id: "42", display_name: "テスト太郎" };

  const fetchUserSpy = jest
    .spyOn(DatabaseClient.prototype, "fetchUser")
    .mockReturnValue(resDbUser);

  const sendUserSpy = jest
    .spyOn(MessageClient.prototype, "sendUser")
    .mockReturnValue(true);

  const act = service.sendUserInfo("42");

  expect(act).toBe(true);

  // 「どう呼ばれたか」を検証(Spy)
  expect(fetchUserSpy).toHaveBeenCalledTimes(1);
  expect(fetchUserSpy).toHaveBeenCalledWith("42");

  expect(sendUserSpy).toHaveBeenCalledTimes(1);
  expect(sendUserSpy).toHaveBeenCalledWith(resDbUser);
});

もし、spyOnした対象にmockReturnValueを指定しない場合、本物の関数がよばれますが、呼び出し記録はされているので、toHaveBeenCalledTimesでそれを検証することができます。

  const sendUserSpy = jest
    .spyOn(MessageClient.prototype, "sendUser");

クラス自体の置き換え例

jest.mock("../src/external", () => ({
  DatabaseClient: jest.fn(),
  MessageClient: jest.fn(),
}));

const service = require("../src/service");
const { DatabaseClient, MessageClient } = require("../src/external");

afterEach(() => {
  jest.restoreAllMocks();
});

test("sendUserInfo: Mock 的に回数と引数を厳密に検証", () => {
  const resDbUser = { id: "42", display_name: "テスト太郎" };

  DatabaseClient.mockImplementation(() => ({
    fetchUser: jest.fn().mockReturnValue(resDbUser),
  }));

  const sendUserMock = jest.fn().mockReturnValue(true);

  MessageClient.mockImplementation(() => ({
    sendUser: sendUserMock,
  }));

  const act = service.sendUserInfo("42");

  expect(act).toBe(true);

  // 「期待通りの回数・引数でしか呼ばれていないこと」をここで検証
  expect(sendUserMock).toHaveBeenCalledTimes(1);
  expect(sendUserMock).toHaveBeenCalledWith(resDbUser);
});

実世界のまとめ

実世界ではGerard Meszarosが示した古典的なテストダブルの定義になっていません。
テストダブル全体を指す用語としてモックが使われることがあります。
狭義のモックオブジェクトについては事前に期待値を登録するとは限らず、使用状況を記録しておき、後で検証するというGerard Meszarosの定義するテストスパイが該当するケースが多いです。
また、Meszarosの定義するスタブだけの機能はテストツールとして特別に提供されるものではなく、Meszarosの定義するテストスパイやモックオブジェクトが使用されています。
スパイといった場合、Gerard Meszarosの定義した使用状況を記録するスタブという使い方と実関数の使用状況を記録する使い方になっている場合があります。

今日では古典的なテストダブルの分類を使用して明確に分けるというのは不可能で、複数の性質が混ざり合わざるを得ない状況です。

その他のテストダブル

「コード置き換え」から一段引いて見たときに、サブシステム/インフラ丸ごとを差し替えるものも広義のテストダブルと言えると思います。
そういったものの例をいくつか紹介します。

  • インメモリDB
    • テスト中にデータベースをインメモリ実装に置き換える。
      • SQLite の :memory: を使う、MySQL/PostgreSQL を Docker 上でメモリストレージにする等。
    • SQLiteのmemoryに簡単に置き換えたり、DockerでMySQLなどをテスト用に立ててる場合は、そのストレージをmemoryにする。
    • Meszaros 的分類だと代表的なFake(本物に近い動作をする代替実装)とされる。
  • Mock Service Worker
    • ブラウザ/Node.js で HTTP/GraphQL のリクエストを ネットワーク層でインターセプト してモックオブジェクトとするライブラリ。
    • サブシステムとしての Web API を Stub/Fake として置き換える ことができる。
  • WireMock
    • JVM 向けの HTTP モックサーバ。Java ライブラリとしてもスタンドアロンプロセスとしても動作
    • 「外部 HTTP サービスのスタンドアロン Fake 兼 Mock」として使える。
    • プロキシとして動かして本物の通信を記録してスタブを作成することができる。
  • BigQuery Emulator
    • 動作するBigQueryのエミュレータ。Dockerコンテナとして動作する。
    • 本物との挙動差はあるとはいえ、ローカルでSQLの動作確認ができる。Fakeにあたる。
  • fake-gcs-server
    • Google Cloud Storage API をエミュレートする OSS。スタンドアロン or Docker
    • Fakeにあたる。
  • moto
    • AWS の各サービスをローカルでエミュレートする。Fakeにあたる
    • pythonのみ
  • LocalStack
    • AWSのSES,SQS,DynamoDBなどの動作をエミュレートするコンテナ。Fakeといえる
    • 言語依存はなく、ローカルで AWS 一部サービスの動作確認が行える
    • 課金をすることで使用できる機能が増える
  • MailHog
    • Fake SMTP サーバ。アプリの SMTP 送信先を MailHog に向けると、メールは実際の外部には送られず、Web UI や API 経由で確認できる。
    • 誤送信を防ぎつつ、メール本文やリンクを UI から確認できるので、メール配送サブシステムの Fakeにあたる

まとめ

今回はGerard Meszaros 氏が整理した古典的なテストダブルを確認しました。
実世界では、その定義は厳密には使用されておらず、テストダブル全体をモックという傾向があります。
狭義のモックやスパイの意味についても、各フレームワークごとに微妙な違いが存在します。
これは特定のフレームワークを使っていた作業者が、思い込みで別のフレームワークを使うと予期せぬ挙動に遭遇するケースがあることを意味します。
各テストダブルのフレームワークを使用する際は、そのツールがいうモック、スパイ、スタブが何を差しているかを確認してから使用することが望ましいと思われます。

では今日ではGerard Meszaros 氏の分類を意識するのが無意味かというとそうではありません。
今日ではテストダブルのフレームワークは複数の性質が混ざり合わさっている状況です。その複数の性質を分解して理解するということには一定の意義があると思います。

Discussion