- 検証・テスト
テストダブルとは|スタブ・モック・スパイ・フェイクの違いと使い分け
テストダブルは、テストのときに本物の依存先(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期連続の黒字決算。