メインコンテンツまでスキップ

第44章:モック/スパイ(呼ばれ方を仕様に)🎭📣

スパイのマイク

🎯 この章のゴール

  • 「副作用(通知・ログ・イベント発火など)」を テストで保証できるようになるよ✨
  • 「返り値」じゃなくて “呼ばれ方”(呼ぶ/呼ばない・回数・引数)を仕様として固定できるよ🧷
  • モック(vi.fn)とスパイ(vi.spyOn)の 使い分けができるよ🎯 (Vitest)

📚 今日覚えるコト(超だいじ)🧠✨

1) モックとスパイ、ざっくり何が違うの?👀

  • モック(vi.fn):ニセの関数を自分で作る🎭 → 依存を「差し替えて」呼ばれ方を見るのが得意✨ (Vitest)
  • スパイ(vi.spyOn):すでにあるオブジェクトのメソッドに「盗聴器」を付ける📣 → 「どんなふうに呼ばれたか」を記録して、必要なら挙動も差し替えできるよ (Vitest)

どっちも「呼び出し履歴」を持つし、同じようなメソッド(mockImplementationOnceとか)を使えるよ✅ (Vitest)


2) “呼ばれ方を仕様にする”ってどういう意味?🧾

例:注文確定で「通知を送る」📨

  • ✅ 注文OK → 通知を 1回 送る
  • ✅ 引数は「誰に」「何を」
  • ✅ 注文NG → 通知は 送らない

この「送る/送らない・回数・引数」が、そのまま仕様になる感じだよ🫶


🧪 手を動かす(ミニ題材)🍰

題材:注文が確定したら通知する📦➡️📨

「placeOrder(注文確定)」が、依存の notifier.send を呼ぶかどうかをテストするよ!


🧩 まずは “モック(vi.fn)” でいくパターン🎭

✅ 仕様

  • 合計金額 total が 1以上なら notifier.send が 1回呼ばれる
  • total が 0なら呼ばれない

1) テストを書く(Red)🔴

// src/order.ts
export type Order = { userId: string; total: number }
export type Notifier = { send: (userId: string, message: string) => void }

export function placeOrder(order: Order, deps: { notifier: Notifier }) {
// ここはあとで実装(最初は空でOK)
}
// src/order.test.ts
import { describe, it, expect, vi } from 'vitest'
import { placeOrder, type Notifier } from './order'

describe('placeOrder', () => {
it('total >= 1 のとき通知を1回送る 📩', () => {
const send = vi.fn()
const notifier: Notifier = { send }

placeOrder({ userId: 'u1', total: 1200 }, { notifier })

expect(send).toHaveBeenCalledTimes(1)
expect(send).toHaveBeenCalledWith('u1', '注文が確定しました')
})

it('total = 0 のとき通知しない 🙅‍♀️', () => {
const send = vi.fn()
const notifier: Notifier = { send }

placeOrder({ userId: 'u1', total: 0 }, { notifier })

expect(send).not.toHaveBeenCalled()
})
})

※「toHaveBeenCalledTimes / toHaveBeenCalledWith」みたいな呼び方チェックができるよ✅ (Vitest)


2) 最小実装(Green)🟢

// src/order.ts
export type Order = { userId: string; total: number }
export type Notifier = { send: (userId: string, message: string) => void }

export function placeOrder(order: Order, deps: { notifier: Notifier }) {
if (order.total <= 0) return
deps.notifier.send(order.userId, '注文が確定しました')
}

3) リファクタ(Refactor)🧹✨

ここでやりがちなのが「通知文言があちこちに散る」問題😵‍💫 → 文字列をまとめたり、条件判定を小関数にしたりしてOKだよ!


🕵️ 次は “スパイ(vi.spyOn)” のパターン📣

「依存として注入してない既存オブジェクト」を監視したいときに便利!

例:logger.info が呼ばれたかを確認📝

import { describe, it, expect, vi, afterEach } from 'vitest'

describe('logger の呼ばれ方を見る 📣', () => {
afterEach(() => {
vi.restoreAllMocks()
})

it('info が1回呼ばれる 🗣️', () => {
const logger = {
info: (msg: string) => { /* 本当はここでログ出す */ },
}

const spy = vi.spyOn(logger, 'info').mockImplementation(() => {})

logger.info('hello')

expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith('hello')
})
})
  • vi.spyOn は「既存メソッドにスパイを貼る」感じだよ📌 (Vitest)
  • 後始末はけっこう大事!テストの外に影響を残しがちなので、まとめて元に戻すのが安心🧯 (Vitest)

🧼 後片付け:clear / reset / restore の使い分け🧽✨

ここ、テストが不安定になる原因No.1になりがち!🥺

  • vi.clearAllMocks:呼び出し履歴だけ消す(実装はそのまま)🧼 (Vitest)
  • vi.resetAllMocks:履歴も消すし、モック実装もリセット🧯 (Vitest)
  • vi.restoreAllMocks:spyOn したものを “元の実装に戻す” 🏠 ただし「履歴は消えない」などクセがあるよ🧠 (Vitest)

👉 迷ったらこの運用がラク:

  • vi.fn を多用 → 基本は clearAllMocks でOK
  • vi.spyOn を使う → afterEach で restoreAllMocks(元に戻すのが最優先) (Vitest)

⚠️ よくある落とし穴(ここハマりやすい)🕳️💥

1) “ログが呼ばれた” を仕様にしすぎる😇

  • ログは実装都合で変わりやすいので、大事な副作用だけを仕様にするのがおすすめ✨ (例:ユーザー通知、決済、メール送信、イベント発火 など)

2) モジュールを mock したのに効かない?🤔

「外から呼ばれた分」は差し替わっても、同じモジュール内で直接呼んでる関数は差し替わらないことがあるよ⚠️ (Vitest) → この場合は「設計として依存を外から渡す」形に寄せるとスッキリすることが多いよ🧩

3) クラス/コンストラクタを spyOn して変なエラー🧨

クラス系を差し替えるとき、書き方によっては「コンストラクタじゃない」系で落ちることがあるよ(矢印関数とか)💦 (Vitest) → そういうときは docs の例みたいに function / class で差し替えるのが安全🛡️ (Vitest)


🤖 AIの使いどころ(この章向け)✨

AIに頼むと強いこと💪

  • 「この処理の副作用って何?」を洗い出してもらう👀
  • 「呼ばれ方として仕様にすべき観点」を列挙してもらう🗂️
  • テスト名の改善案を出してもらう📝

コピペ用プロンプト例(そのまま使ってOK)🪄

  • 「この関数の副作用(呼ばれ方で仕様にすべき点)を列挙して。回数・引数・呼ぶ/呼ばないの観点で。」
  • 「Vitestで vi.fn / vi.spyOn を使って “呼ばれ方” を検証するテスト案を2通り(モック/スパイ)出して。過剰に細かくしない方針で。」 (Vitest)

✅ チェックリスト(できたら合格💮)

  • ✅ モック(vi.fn)で「呼ぶ/呼ばない」「回数」「引数」をテストできた
  • ✅ スパイ(vi.spyOn)で既存メソッドの呼ばれ方を検証できた (Vitest)
  • ✅ テスト後に restoreAllMocks / clearAllMocks のどちらが必要か説明できる (Vitest)
  • ✅ “仕様にすべき呼ばれ方” と “実装都合の呼ばれ方” を分けて考えられた🎯

🌟 ミニ課題(10〜20分)⏰💖

次のどれか1つだけやってみてね!(短くてOK✨)

  1. 「ログイン成功で analytics.track を呼ぶ」📊
  2. 「購入成功で mailer.send を呼ぶ」📨
  3. 「失敗時は notifier.send を呼ばない」🙅‍♀️

ポイント

  • 大事なのは “何を返すか” じゃなくて “何を呼ぶか” 🎭📣

次の第45章は「ファイルI/O境界(本物/偽物の判断)📁」だから、今回の“副作用を境界で扱う感覚”がそのまま効いてくるよ〜!🧠✨