テストダブルは、テストのときに本物の依存先(DB・外部 API・メール送信など)の代わりに置く「代役」の総称です。 映画のスタントマン(stunt double)から来た言葉で、Gerard Meszaros が著書『xUnit Test Patterns』で5種類に分けました。

5種類の違いは「テスト対象に値を渡すのか」「テスト対象からの呼び出しを確かめるのか」で分かれます。

種類 ひとことで 役割 確かめ方
ダミー(Dummy) 引数を埋めるだけ 使われない 確かめない
スタブ(Stub) 決まった値を返す 間接入力を与える 戻り値・状態(状態の検証)
スパイ(Spy) 呼ばれ方を記録する 間接出力を記録する テストの最後に記録を見る
モック(Mock) 期待を先に持つ 間接出力を照合する 呼ばれた時点で期待と照合(振る舞いの検証)
フェイク(Fake) 簡単な本物 本物の代わりに動く 確かめる道具には使わない

間接入力は「テスト対象が依存先から受け取る値」(例:税率 API の戻り値)、間接出力は「テスト対象が依存先へ送る呼び出し」(例:メール送信)です。

確認環境:Python 3.11.15 の unittest・unittest.mock と、Vitest 5.0.4(Node.js 22.22.2)で、この記事のテストを実行しました(Linux)。Java のコードは Mockito 5.24.0 の Javadoc で API を確かめた形です。5分類の定義は Martin Fowler の「TestDouble」と xUnit Patterns のページで確かめました(確認日 2026年10月13日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「テストダブルとは|スタブ・モック・スパイ・フェイクの違いと使い分け」

5分類の定義(原文と要点)

Martin Fowler は2006年1月17日の記事「TestDouble」で、Meszaros の用語を次のように紹介しています。

種類 原文(Fowler「TestDouble」より) 要点
Dummy passed around but never actually used. Usually they are just used to fill parameter lists. 引数の数合わせ
Fake actually have working implementations, but usually take some shortcut 動くが手を抜いた実装(メモリ上の DB など)
Stub provide canned answers to calls made during the test 用意した答えを返す
Spy stubs that also record some information based on how they were called 記録もするスタブ
Mock pre-programmed with expectations which form a specification of the calls they are expected to receive 受けるはずの呼び出しを、先に期待として持つ

xUnit Patterns の「Test Double」のページでは、スタブは「間接入力の制御点(control point)」、スパイとモックは「間接出力の観測点(observation point)」と説明されています。フェイクはそのどちらにも使わない、とされています。

スパイとモックの違い

一番まぎらわしいのが、スパイとモックです。どちらも「呼ばれたか」を確かめます。

スパイ モック
期待をいつ決めるか テスト対象を動かしたあと テスト対象を動かす前
誤りに気づくとき テストの最後の assert 呼ばれたその時点(最後に verify も)
テストの読み方 準備 → 実行 → 確認(スタブのテストと同じ形) 準備(期待も書く)→ 実行 → verify

Mockito の verify、unittest.mock の assert_called_once_with、Vitest の toHaveBeenCalledWith は、どれも「あとから記録を確かめる」形なので、Meszaros の分類ではスパイの使い方です。 ライブラリの名前が「mock」でも、使い方によってスタブにもスパイにもなります。

題材:注文を受け付けるサービス

3つの言語で、同じ処理を題材にします。

# order_service.py
class OrderService:
    def __init__(self, tax_rates, orders, mailer, logger):
        self.tax_rates = tax_rates  # 税率を返す(間接入力)
        self.orders = orders        # 注文を保存する
        self.mailer = mailer        # メールを送る(間接出力)
        self.logger = logger        # 失敗したときだけ使う

    def place(self, email, amount):
        if amount <= 0:
            self.logger.warning("invalid amount: %s", amount)
            raise ValueError("amount must be positive")
        rate = self.tax_rates.rate_for("JP")
        total = round(amount * (1 + rate))
        order_id = self.orders.save({"email": email, "total": total})
        self.mailer.send(email, f"注文{order_id}を受け付けました。合計{total}円")
        return order_id, total
依存先 本物 テストでの代役
tax_rates 外部の税率 API スタブ(0.1 を返す)
orders DB フェイク(メモリ上に保存)
mailer SMTP スパイかモック
logger ログ出力 ダミー(正常系では使われない)

Python:手書きのテストダブル(5種類)

ライブラリを使わずに書くと、5種類の違いがコードの形にそのまま出ます。

# doubles.py
class DummyLogger:
    """ダミー:引数を埋めるだけ。呼ばれたら失敗させる"""
    def warning(self, *args):
        raise AssertionError("ダミーは使われないはず")


class StubTaxRates:
    """スタブ:決まった値を返す"""
    def __init__(self, rate):
        self.rate = rate

    def rate_for(self, country):
        return self.rate


class SpyMailer:
    """スパイ:呼ばれ方を記録する(確かめるのはテスト側)"""
    def __init__(self):
        self.sent = []

    def send(self, to, body):
        self.sent.append((to, body))


class MockMailer:
    """モック:期待を先に持ち、呼ばれた時点で照合する"""
    def __init__(self):
        self.expected = []

    def expect_send(self, to, body):
        self.expected.append((to, body))

    def send(self, to, body):
        if not self.expected:
            raise AssertionError(f"予期しない呼び出し: send({to!r}, ...)")
        exp = self.expected.pop(0)
        if exp != (to, body):
            raise AssertionError(f"期待 {exp!r} / 実際 {(to, body)!r}")

    def verify(self):
        if self.expected:
            raise AssertionError(f"呼ばれなかった: {self.expected!r}")


class FakeOrderRepository:
    """フェイク:本物の代わりに動く簡単な実装(メモリ上の保存)"""
    def __init__(self):
        self.rows = {}
        self.next_id = 1

    def save(self, row):
        order_id = self.next_id
        self.rows[order_id] = row
        self.next_id += 1
        return order_id

    def find(self, order_id):
        return self.rows.get(order_id)
# test_handwritten.py
import unittest
from order_service import OrderService
from doubles import DummyLogger, StubTaxRates, SpyMailer, MockMailer, FakeOrderRepository


class HandwrittenDoublesTest(unittest.TestCase):
    def setUp(self):
        self.orders = FakeOrderRepository()          # フェイク
        self.spy = SpyMailer()                       # スパイ
        self.service = OrderService(StubTaxRates(0.1), self.orders, self.spy, DummyLogger())

    def test_stub_税率10パーセントで合計を計算する(self):
        _, total = self.service.place("a@example.com", 1000)
        self.assertEqual(total, 1100)

    def test_fake_保存した注文を読み出せる(self):
        order_id, _ = self.service.place("a@example.com", 1000)
        self.assertEqual(self.orders.find(order_id), {"email": "a@example.com", "total": 1100})

    def test_spy_メールが1通送られた(self):
        self.service.place("a@example.com", 1000)
        self.assertEqual(self.spy.sent, [("a@example.com", "注文1を受け付けました。合計1100円")])

    def test_mock_期待どおりに呼ばれた(self):
        mock = MockMailer()
        mock.expect_send("a@example.com", "注文1を受け付けました。合計1100円")
        service = OrderService(StubTaxRates(0.1), FakeOrderRepository(), mock, DummyLogger())
        service.place("a@example.com", 1000)
        mock.verify()

    def test_mock_違う本文なら呼ばれた時点で失敗する(self):
        mock = MockMailer()
        mock.expect_send("a@example.com", "合計1000円")
        service = OrderService(StubTaxRates(0.1), FakeOrderRepository(), mock, DummyLogger())
        with self.assertRaises(AssertionError) as cm:
            service.place("a@example.com", 1000)
        print("\n  mock の失敗:", cm.exception)


if __name__ == "__main__":
    unittest.main(verbosity=2)
$ python3 test_handwritten.py
test_fake_保存した注文を読み出せる ... ok
test_mock_期待どおりに呼ばれた ... ok
test_mock_違う本文なら呼ばれた時点で失敗する ...
  mock の失敗: 期待 ('a@example.com', '合計1000円') / 実際 ('a@example.com', '注文1を受け付けました。合計1100円')
ok
test_spy_メールが1通送られた ... ok
test_stub_税率10パーセントで合計を計算する ... ok

Ran 5 tests in 0.000s

OK

(出力のテスト名の後ろのクラス名は省略しています)

モックは mailer.send が呼ばれた瞬間に例外を出し、place の途中で止まりました。 スパイなら place は最後まで動き、テストの最後の assertEqual で失敗します。この「どこで気づくか」が、スパイとモックの違いです。

Python:unittest.mock で書く

unittest.mock は1つの道具で、スタブにもスパイにもなります。 create_autospec を使うと、本物のクラスにないメソッドや、引数の数の誤りで止まります。

# collaborators.py(本物の依存先。テストでは置き換える)
class TaxRates:
    def rate_for(self, country):
        raise NotImplementedError("外部の税率APIを呼ぶ")


class Mailer:
    def send(self, to, body):
        raise NotImplementedError("SMTPで送る")


class OrderRepository:
    def save(self, row):
        raise NotImplementedError("DBに保存する")


class Logger:
    def warning(self, msg, *args):
        print(msg % args)
# test_unittest_mock.py
import unittest
from unittest.mock import Mock, create_autospec
from order_service import OrderService
from collaborators import TaxRates, Mailer, OrderRepository, Logger
from doubles import FakeOrderRepository


class UnittestMockTest(unittest.TestCase):
    def setUp(self):
        self.tax = create_autospec(TaxRates, instance=True)
        self.tax.rate_for.return_value = 0.1                 # スタブ:値を返す
        self.mailer = create_autospec(Mailer, instance=True)  # スパイ:記録する
        self.logger = create_autospec(Logger, instance=True)  # ダミー(使われない)
        self.orders = FakeOrderRepository()                   # フェイク
        self.service = OrderService(self.tax, self.orders, self.mailer, self.logger)

    def test_stub(self):
        _, total = self.service.place("a@example.com", 1000)
        self.assertEqual(total, 1100)

    def test_spy(self):
        self.service.place("a@example.com", 1000)
        self.mailer.send.assert_called_once_with("a@example.com", "注文1を受け付けました。合計1100円")
        self.tax.rate_for.assert_called_once_with("JP")

    def test_dummy_は使われていない(self):
        self.service.place("a@example.com", 1000)
        self.logger.warning.assert_not_called()

    def test_stub_で例外を返す(self):
        self.tax.rate_for.side_effect = TimeoutError("tax api timeout")
        with self.assertRaises(TimeoutError):
            self.service.place("a@example.com", 1000)
        self.mailer.send.assert_not_called()

    def test_repository_を_Mock_にした場合(self):
        orders = create_autospec(OrderRepository, instance=True)
        orders.save.return_value = 42
        service = OrderService(self.tax, orders, self.mailer, self.logger)
        self.assertEqual(service.place("a@example.com", 1000), (42, 1100))

    def test_spec_なしだと打ち間違いも通る(self):
        loose = Mock()
        loose.sned("a@example.com", "x")          # 打ち間違い
        print("\n  spec なし:", loose.sned.call_count, "回")
        strict = create_autospec(Mailer, instance=True)
        with self.assertRaises(AttributeError) as cm:
            strict.sned("a@example.com", "x")
        print("  autospec:", cm.exception)

    def test_引数の数の誤りも_autospec_なら止まる(self):
        strict = create_autospec(Mailer, instance=True)
        with self.assertRaises(TypeError) as cm:
            strict.send("a@example.com")
        print("\n  autospec:", cm.exception)

    def test_失敗したときのメッセージ(self):
        self.service.place("a@example.com", 1000)
        with self.assertRaises(AssertionError) as cm:
            self.mailer.send.assert_called_once_with("a@example.com", "合計1000円")
        print("\n" + str(cm.exception))


if __name__ == "__main__":
    unittest.main(verbosity=2)
$ python3 test_unittest_mock.py
test_dummy_は使われていない ... ok
test_repository_を_Mock_にした場合 ... ok
test_spec_なしだと打ち間違いも通る ...
  spec なし: 1 回
  autospec: Mock object has no attribute 'sned'
ok
test_spy ... ok
test_stub ... ok
test_stub_で例外を返す ... ok
test_失敗したときのメッセージ ...
expected call not found.
Expected: send('a@example.com', '合計1000円')
  Actual: send('a@example.com', '注文1を受け付けました。合計1100円')
ok
test_引数の数の誤りも_autospec_なら止まる ...
  autospec: missing a required argument: 'body'
ok

Ran 8 tests in 0.030s

OK

Mock() を spec なしで作ると、sned のような打ち間違いのメソッドも呼べてしまい、テストが通りました。 create_autospec(本物のクラス, instance=True) にすると AttributeError で止まります。

unittest.mock の書き方 分類
m.method.return_value = 値 スタブ
m.method.side_effect = 例外 スタブ(例外を返す)
m.method.assert_called_once_with(...) スパイ(あとから確かめる)
m.method.assert_not_called() ダミーが使われていないことの確認
patch.object(obj, "method", wraps=obj.method) 本物を呼びつつ記録するスパイ

wraps= を使うと、本物の処理を動かしたまま呼び出しを記録できました。フェイクの save を包むと、戻り値は本物どおり 1、call_args は call({'email': 'a'}) でした。

unittest の assert の一覧は、関連記事「Python unittestの使い方|assert一覧とsetUp・mock」で扱っています。pytest で書くなら、関連記事「pytest 使い方|導入からfixture・mock・カバレッジまで実行結果つき」で扱っています。

JavaScript:Vitest で書く

// orderService.js
export class OrderService {
  constructor(taxRates, orders, mailer, logger) {
    this.taxRates = taxRates
    this.orders = orders
    this.mailer = mailer
    this.logger = logger
  }

  place(email, amount) {
    if (amount <= 0) {
      this.logger.warn(`invalid amount: ${amount}`)
      throw new Error('amount must be positive')
    }
    const rate = this.taxRates.rateFor('JP')
    const total = Math.round(amount * (1 + rate))
    const orderId = this.orders.save({ email, total })
    this.mailer.send(email, `注文${orderId}を受け付けました。合計${total}円`)
    return { orderId, total }
  }
}

export class FakeOrderRepository {
  rows = new Map()
  nextId = 1
  save(row) {
    const id = this.nextId++
    this.rows.set(id, row)
    return id
  }
  find(id) {
    return this.rows.get(id)
  }
}

export const realMailer = {
  send(to, body) {
    throw new Error('SMTPで送る')
  },
}
// orderService.test.js
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { OrderService, FakeOrderRepository, realMailer } from './orderService.js'

describe('テストダブル(Vitest)', () => {
  let taxRates, orders, mailer, logger, service

  beforeEach(() => {
    taxRates = { rateFor: vi.fn().mockReturnValue(0.1) } // スタブ
    orders = new FakeOrderRepository()                    // フェイク
    mailer = { send: vi.fn() }                            // スパイ(記録する)
    logger = {}                                           // ダミー(中身なし)
    service = new OrderService(taxRates, orders, mailer, logger)
  })

  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('stub: 税率10%で合計を計算する', () => {
    expect(service.place('a@example.com', 1000).total).toBe(1100)
  })

  it('fake: 保存した注文を読み出せる', () => {
    const { orderId } = service.place('a@example.com', 1000)
    expect(orders.find(orderId)).toEqual({ email: 'a@example.com', total: 1100 })
  })

  it('spy: メールが1通送られた', () => {
    service.place('a@example.com', 1000)
    expect(mailer.send).toHaveBeenCalledTimes(1)
    expect(mailer.send).toHaveBeenCalledWith('a@example.com', '注文1を受け付けました。合計1100円')
  })

  it('spyOn: 本物のオブジェクトの1メソッドだけ置き換える', () => {
    const spy = vi.spyOn(realMailer, 'send').mockImplementation(() => {})
    new OrderService(taxRates, orders, realMailer, logger).place('b@example.com', 500)
    expect(spy).toHaveBeenCalledWith('b@example.com', '注文1を受け付けました。合計550円')
  })

  it('mock: 期待と違う呼び出しはその場で失敗させる', () => {
    const expected = ['a@example.com', '合計1000円']
    const mockMailer = {
      send: vi.fn((...args) => {
        expect(args).toEqual(expected)
      }),
    }
    const s = new OrderService(taxRates, orders, mockMailer, logger)
    expect(() => s.place('a@example.com', 1000)).toThrow(/expected/)
  })

  it('stub: 例外を返す', () => {
    taxRates.rateFor.mockImplementation(() => {
      throw new Error('tax api timeout')
    })
    expect(() => service.place('a@example.com', 1000)).toThrow('tax api timeout')
    expect(mailer.send).not.toHaveBeenCalled()
  })
})
$ npx vitest run --reporter=verbose
 ✓ orderService.test.js > テストダブル(Vitest) > stub: 税率10%で合計を計算する 7ms
 ✓ orderService.test.js > テストダブル(Vitest) > fake: 保存した注文を読み出せる 4ms
 ✓ orderService.test.js > テストダブル(Vitest) > spy: メールが1通送られた 2ms
 ✓ orderService.test.js > テストダブル(Vitest) > spyOn: 本物のオブジェクトの1メソッドだけ置き換える 3ms
 ✓ orderService.test.js > テストダブル(Vitest) > mock: 期待と違う呼び出しはその場で失敗させる 5ms
 ✓ orderService.test.js > テストダブル(Vitest) > stub: 例外を返す 1ms
 Test Files  1 passed (1)
      Tests  6 passed (6)

ダミーの logger = {} は中身のない空のオブジェクトです。 正常系では logger.warn を呼びません。もし呼ばれると TypeError で失敗するので、使われていないことの確認にもなります。

toHaveBeenCalledWith で本文を '合計1000円' にして失敗させると、次の差分が出ました。

AssertionError: expected "vi.fn()" to be called with arguments: [ 'a@example.com', '合計1000円' ]

Received:

  1st vi.fn() call:

  [
    "a@example.com",
-   "合計1000円",
+   "注文1を受け付けました。合計1100円",
  ]


Number of calls: 1
Vitest の書き方 分類
vi.fn().mockReturnValue(値) スタブ
vi.fn().mockImplementation(() => { throw ... }) スタブ(例外を返す)
vi.fn() + toHaveBeenCalledWith スパイ
vi.spyOn(obj, 'method') 本物を呼びつつ記録するスパイ(mockImplementation で置き換えも可)
vi.fn((...args) => expect(args)...) モック(呼ばれた時点で照合)

vi.mock でモジュールごと置き換える方法は、関連記事「Vitestモックの使い方|vi.mock・vi.fn・vi.spyOnの使い分け表」で扱っています。

Java:Mockito で書く

Mockito の mock() も、使い方でスタブにもスパイ(記録の確認)にもなります。spy() は、Javadoc に「スタブしていないメソッドは本物が呼ばれる」とある、本物を包むテストダブルです。

// OrderService.java
public class OrderService {
  private final TaxRates taxRates;
  private final OrderRepository orders;
  private final Mailer mailer;
  private final Logger logger;

  public OrderService(TaxRates taxRates, OrderRepository orders, Mailer mailer, Logger logger) {
    this.taxRates = taxRates;
    this.orders = orders;
    this.mailer = mailer;
    this.logger = logger;
  }

  public long place(String email, long amount) {
    if (amount <= 0) {
      logger.warn("invalid amount: " + amount);
      throw new IllegalArgumentException("amount must be positive");
    }
    double rate = taxRates.rateFor("JP");
    long total = Math.round(amount * (1 + rate));
    long orderId = orders.save(new Order(email, total));
    mailer.send(email, "注文" + orderId + "を受け付けました。合計" + total + "円");
    return total;
  }
}

interface TaxRates { double rateFor(String country); }
interface OrderRepository { long save(Order order); }
interface Mailer { void send(String to, String body); }
interface Logger { void warn(String message); }
record Order(String email, long total) {}
// FakeOrderRepository.java(フェイクは手書き)
import java.util.*;

class FakeOrderRepository implements OrderRepository {
  final Map<Long, Order> rows = new HashMap<>();
  private long nextId = 1;

  @Override
  public long save(Order order) {
    long id = nextId++;
    rows.put(id, order);
    return id;
  }
}
// OrderServiceTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

  @Mock TaxRates taxRates;   // スタブとして使う
  @Mock Mailer mailer;       // スパイとして使う(verify)
  @Mock Logger logger;       // ダミー

  @Test
  void stub_税率10パーセントで合計を計算する() {
    when(taxRates.rateFor("JP")).thenReturn(0.1);
    OrderService service = new OrderService(taxRates, new FakeOrderRepository(), mailer, logger);

    assertEquals(1100, service.place("a@example.com", 1000));
  }

  @Test
  void fake_保存した注文を読み出せる() {
    when(taxRates.rateFor("JP")).thenReturn(0.1);
    FakeOrderRepository orders = new FakeOrderRepository();
    new OrderService(taxRates, orders, mailer, logger).place("a@example.com", 1000);

    assertEquals(new Order("a@example.com", 1100), orders.rows.get(1L));
  }

  @Test
  void spy_メールが1通送られた() {
    when(taxRates.rateFor("JP")).thenReturn(0.1);
    new OrderService(taxRates, new FakeOrderRepository(), mailer, logger).place("a@example.com", 1000);

    verify(mailer).send("a@example.com", "注文1を受け付けました。合計1100円");
    verifyNoInteractions(logger);   // ダミーが使われていない
  }

  @Test
  void stub_例外を返す() {
    when(taxRates.rateFor("JP")).thenThrow(new RuntimeException("tax api timeout"));
    OrderService service = new OrderService(taxRates, new FakeOrderRepository(), mailer, logger);

    assertThrows(RuntimeException.class, () -> service.place("a@example.com", 1000));
    verify(mailer, never()).send(anyString(), anyString());
  }

  @Test
  void spy_本物を包む() {
    when(taxRates.rateFor("JP")).thenReturn(0.1);
    FakeOrderRepository orders = spy(new FakeOrderRepository());
    new OrderService(taxRates, orders, mailer, logger).place("a@example.com", 1000);

    verify(orders).save(new Order("a@example.com", 1100));   // 呼ばれ方を記録
    assertEquals(1, orders.rows.size());                       // 本物の処理も動いた
  }
}
Mockito の書き方 分類
when(m.x()).thenReturn(値) スタブ
when(m.x()).thenThrow(例外) スタブ(例外を返す)
verify(m).x(...) スパイ(あとから確かめる)
verifyNoInteractions(m) ダミーが使われていないことの確認
spy(本物) 本物を呼びつつ記録するスパイ

Javadoc では、spy のメソッドをスタブするときは when(...) ではなく doReturn(...).when(spy).x() の形を検討するよう書かれています。when(spy.x()) の時点で本物の x() が呼ばれてしまうためです。また、MockitoExtension では、使われなかったスタブ(when を書いたのに呼ばれなかったもの)が報告されます。Mockito の基本は、関連記事「Mockitoの使い方|JUnit 5での基本からよくあるエラーまで」で扱っています。

3言語の対応表

分類 Python unittest.mock Vitest Mockito
ダミー create_autospec(Logger, instance=True) {} @Mock Logger
スタブ .return_value / .side_effect mockReturnValue / mockImplementation when().thenReturn() / thenThrow()
スパイ assert_called_once_with toHaveBeenCalledWith verify()
本物を包むスパイ patch.object(..., wraps=...) vi.spyOn spy()
フェイク 手書きのクラス 手書きのクラス 手書きのクラス

フェイクは、どの言語でもライブラリではなく手書きです。 メモリ上のリポジトリのほか、SQLite のメモリ DB、ローカルで動くメールサーバーなどもフェイクにあたります。

どれを使うか:目安

確かめたいこと 使う代役 例
依存先の戻り値によって結果が変わるか スタブ 税率 0.1 なら 1100 円、API が例外なら中断
依存先に正しく伝えたか(副作用) スパイ(期待を先に決めたいならモック) メールの宛先と本文
保存して読み出す流れ全体 フェイク リポジトリ、キャッシュ
引数は必要だが今回は関係ない ダミー 異常系でしか使わないロガー

目安として、値を「受け取る」呼び出し(問い合わせ)はスタブにして結果を確かめ、何かを「起こす」呼び出し(コマンド)はスパイ・モックで確かめる、と分けると、テストが実装の細部に寄りすぎません。xUnit Patterns のページでも、テストダブルの使いすぎは、実装の細部まで決めつけた壊れやすいテスト(Fragile Test)につながると注意されています。

Fowler の記事「Mocks Aren't Stubs」では、結果の状態で確かめる書き方を状態の検証(state verification)、呼び出しで確かめる書き方を振る舞いの検証(behavior verification)と呼んでいます。

よくある質問

Q. 「モックを使う」と言うとき、厳密にはどれですか。 A. 日常会話では、ライブラリで作った代役をまとめて「モック」と呼ぶことが多いです。Meszaros の分類では、戻り値を返すだけならスタブ、あとから呼び出しを確かめるならスパイです。チームで用語をそろえると、レビューで話が通じやすくなります。

Q. HTTP の API はどれで置き換えますか。 A. HTTP クライアントのクラスをスタブにする方法と、にせものの HTTP サーバー(WireMock・MSW など)を立てる方法があります。後者は、通信やJSON の変換も含めて試せるフェイク寄りの代役です。

Q. Kotlin では。 A. MockK がよく使われます。関連記事「MockK 使い方|every・verify・coEveryとMockito対応表」で扱っています。

Q. 単体テストで全部の依存を置き換えるべきですか。 A. 置き換えるほど速く安定しますが、本物との組み合わせの誤りは見つかりません。xUnit Patterns でも、テストダブルを使わずに本物で動かすテストを少なくとも1つ残すよう勧めています。関連記事「単体テストと結合テストの違い|pytestの実例と実行結果で比較」で扱っています。

まとめ

  • テストダブルは本物の依存先の代役。Meszaros の分類は ダミー・スタブ・スパイ・モック・フェイク
  • スタブは間接入力を与える、スパイ・モックは間接出力を確かめる
  • スパイは「あとで確認」、モックは「先に期待を持ち、呼ばれた時点で照合」
  • Mockito の verify、unittest.mock の assert_called_*、Vitest の toHaveBeenCalledWith はスパイの使い方
  • フェイクは手書き。メモリ上のリポジトリなど
  • unittest.mock は create_autospec で打ち間違いと引数の誤りを止められる

参考・出典

確認日はいずれも 2026年10月13日です。

  • Martin Fowler「TestDouble」:https://martinfowler.com/bliki/TestDouble.html
  • Martin Fowler「Mocks Aren't Stubs」:https://martinfowler.com/articles/mocksArentStubs.html
  • xUnit Patterns「Test Double」:http://xunitpatterns.com/Test%20Double.html
  • Python ドキュメント「unittest.mock」:https://docs.python.org/3/library/unittest.mock.html
  • Vitest ドキュメント「Mocking」:https://vitest.dev/guide/mocking
  • Mockito 5.24.0 Javadoc「Mockito」:https://javadoc.io/static/org.mockito/mockito-core/5.24.0/org.mockito/org/mockito/Mockito.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「テストダブルとは|スタブ・モック・スパイ・フェイクの違いと使い分け」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?