第17章:テストが簡単になる!Fake/Stubで体験🧪🧸✨
この章でできるようになること🎯
- 「DIPするとテストがラクになる理由」を体感できる💡
- Fake / Stub を作って、**“本物なし”**でユースケースをテストできる🧸✨
- テストの目的が「バグ探し」じゃなくて **“仕様を守る契約書”**だとわかる📌📝
1) まず結論:DIPは「テストしやすさ」を爆上げする💪🧪
DIPが効くと、上位(業務ロジック)が DB / HTTP / 時間 / ファイルみたいな“下位の事情”に振り回されなくなるよね🌪️ するとテストで…
- DBを立てなくていい🙅♀️🗄️
- 外部APIを叩かなくていい🙅♀️🌐
- 現在時刻に左右されない🙅♀️⏰
つまり、速い・安定・怖くないテストになる🎉
2) Fake / Stub / Mock をゆるく理解しよう🧸🧪
ここ、最初はざっくりでOK〜!😊
-
Stub(スタブ):決まった返事を返すだけの子📨
- 例:支払い処理は常に「成功」を返す、とか👍
-
Fake(フェイク):動くけど簡易版の“代用品”🧸
- 例:DBの代わりに、メモリ配列に保存するRepository📦
-
Mock(モック):呼び出し回数や引数を“監視”する子👀
- 例:通知が1回送られたことを確認する、とか🔔
この章は Fake と Stub を中心に「うわ、テスト簡単!」を体験するよ✨
3) 例題:注文確定ユースケースを“本物なし”でテストする📦✅
やりたい仕様(これがテストで守る対象)📌
**PlaceOrder(注文確定)**の仕様を、テストで守るよ!
-
支払いが成功したら ✅
- 注文が保存される(Repositoryに保存)💾
CONFIRMEDになる🎉
-
支払いが失敗したら ❌
- 注文は保存されない🙅♀️💾
4) まずは「抽象(ポート)」を用意する🧩🚪
上位(ユースケース)が頼る先は “interface” にするよ〜✨
// src/ports/PaymentGateway.ts
export interface PaymentGateway {
charge(amountYen: number): Promise<{ ok: true } | { ok: false; reason: string }>;
}
// src/ports/OrderRepository.ts
export interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
// src/ports/Clock.ts
export interface Clock {
nowIso(): string; // 例: "2026-01-15T12:00:00.000Z"
}
// src/domain/Order.ts
export type OrderStatus = "PENDING" | "CONFIRMED";
export type Order = {
id: string;
amountYen: number;
status: OrderStatus;
createdAtIso: string;
};
5) ユースケース(上位)は“抽象だけ”を見る👑✨
ここがDIPの主役だよ〜! DBもAPIも時刻も、知らない(=依存してない)💪
// src/app/PlaceOrder.ts
import type { PaymentGateway } from "../ports/PaymentGateway";
import type { OrderRepository } from "../ports/OrderRepository";
import type { Clock } from "../ports/Clock";
import type { Order } from "../domain/Order";
export class PlaceOrder {
constructor(
private readonly payment: PaymentGateway,
private readonly orders: OrderRepository,
private readonly clock: Clock
) {}
async execute(input: { id: string; amountYen: number }): Promise<Order> {
const createdAtIso = this.clock.nowIso();
const order: Order = {
id: input.id,
amountYen: input.amountYen,
status: "PENDING",
createdAtIso,
};
const result = await this.payment.charge(order.amountYen);
if (!result.ok) {
// 仕様:失敗時は保存しない
throw new Error(`PAYMENT_FAILED: ${result.reason}`);
}
const confirmed: Order = { ...order, status: "CONFIRMED" };
await this.orders.save(confirmed);
return confirmed;
}
}
6) テスト用の Stub / Fake を作る🧸🧪✨

Stub:支払いを“成功/失敗”させたいだけ🙂/😢
// test_doubles/StubPaymentGateway.ts
import type { PaymentGateway } from "../src/ports/PaymentGateway";
export class StubPaymentGatewaySuccess implements PaymentGateway {
async charge(_: numberYen: number) {
return { ok: true } as const;
}
}
export class StubPaymentGatewayFail implements PaymentGateway {
constructor(private readonly reason = "card_declined") {}
async charge(_: numberYen: number) {
return { ok: false, reason: this.reason } as const;
}
}
Fake:Repositoryを“メモリで動く簡易DB”にする📦💾
// test_doubles/FakeOrderRepository.ts
import type { OrderRepository } from "../src/ports/OrderRepository";
import type { Order } from "../src/domain/Order";
export class FakeOrderRepository implements OrderRepository {
private store = new Map<string, Order>();
async save(order: Order): Promise<void> {
this.store.set(order.id, order);
}
async findById(id: string): Promise<Order | null> {
return this.store.get(id) ?? null;
}
// テストで便利な“のぞき見”🫣✨
count(): number {
return this.store.size;
}
}
FixedClock:時刻を固定してテストを安定させる⏰🧊
// test_doubles/FixedClock.ts
import type { Clock } from "../src/ports/Clock";
export class FixedClock implements Clock {
constructor(private readonly fixedIso: string) {}
nowIso(): string {
return this.fixedIso;
}
}
7) Vitestでテストを書く(動く体験パート)🧪⚡
テストランナーは Vitest v4系がサクサクで扱いやすいよ⚡(v4.0.x が公開されてる)(Vitest) (もちろん Jest 30 も現役だよ〜)(Jest)
インストール(例)
npm i -D vitest
package.json の例:
{
"type": "module",
"scripts": {
"test": "vitest"
},
"devDependencies": {
"vitest": "^4.0.0"
}
}
8) テスト本体:本物ゼロでユースケースを検証する🎉🧸
// tests/PlaceOrder.test.ts
import { describe, it, expect } from "vitest";
import { PlaceOrder } from "../src/app/PlaceOrder";
import { StubPaymentGatewaySuccess, StubPaymentGatewayFail } from "../test_doubles/StubPaymentGateway";
import { FakeOrderRepository } from "../test_doubles/FakeOrderRepository";
import { FixedClock } from "../test_doubles/FixedClock";
describe("PlaceOrder", () => {
it("支払い成功なら、注文が保存されてCONFIRMEDになる✅", async () => {
const payment = new StubPaymentGatewaySuccess();
const repo = new FakeOrderRepository();
const clock = new FixedClock("2026-01-15T00:00:00.000Z");
const usecase = new PlaceOrder(payment, repo, clock);
const result = await usecase.execute({ id: "order-1", amountYen: 1200 });
expect(result.status).toBe("CONFIRMED");
expect(result.createdAtIso).toBe("2026-01-15T00:00:00.000Z");
const saved = await repo.findById("order-1");
expect(saved?.status).toBe("CONFIRMED");
expect(repo.count()).toBe(1);
});
it("支払い失敗なら、保存されない&例外になる❌", async () => {
const payment = new StubPaymentGatewayFail("network_error");
const repo = new FakeOrderRepository();
const clock = new FixedClock("2026-01-15T00:00:00.000Z");
const usecase = new PlaceOrder(payment, repo, clock);
await expect(usecase.execute({ id: "order-2", amountYen: 500 }))
.rejects.toThrow("PAYMENT_FAILED");
expect(repo.count()).toBe(0);
});
});
ここが気持ちいいポイント😍✨
- DBもAPIも不要!なのに仕様が守れる🧸
- テストが速い(だから回せる)⚡
- 失敗したとき原因がわかりやすい🔍
これが DIPのごほうびだよ〜🎁💕
9) ありがちなミスとコツ🧠🪄
- テストで本物を使い始める(DB/HTTP直叩き)→ 遅い・不安定になりがち😵
- 時間を固定しない → 日付またぎで落ちる、環境でズレる⏰💥
- テストが“実装”に寄りすぎる → 仕様(期待する振る舞い)に寄せよう📌✨
章末まとめ(3行)🧾✨
- DIPで上位は「抽象」にだけ依存する🧩
- だからテストでは Fake/Stub に差し替えできる🧸
- 結果、速くて安定した“仕様を守るテスト”が作れる🧪✅
ミニ演習(1〜2問)✍️😊
PlaceOrderに「amountYen が 0 以下なら例外」を追加して、テストも1本足してみてね💥🧪FakeOrderRepositoryにall()を作って「保存された注文一覧」を検証するテストを書いてみよう📚✨
AIに聞く用プロンプト例🤖💬(比較させる系✨)
- 「このユースケースのテストで、Stub と Fake をどう分けるのが自然?理由もセットで3パターン出して」🧠🧸
- 「このテストが“実装寄り”になってる部分を指摘して、“仕様寄り”に書き換える案を提案して」🔍📌
- 「Mock を使うべき場面にしたい。通知ポートを追加して“呼び出し回数を検証するテスト”例を作って」🔔👀
次の第18章では「やりすぎ注意⚠️」へ進んで、どこまでDIPするのがちょうどいいかを身につけるよ〜🐍⚖️😊