第16章:モックとスタブを怖がらない🧸🧪✨
この章のゴールはシンプル!👇 **「外部依存(時間・乱数・HTTP・DB)を、テストで“自分の都合のいい状態”にできる」**ようになることです😎💪 そして同時に、初心者がやりがちな “やりすぎモック地獄” を回避します😇🕳️
1) まず用語を“ざっくり”整理しよう🧠✨
厳密さより「使いどころ」が大事!💡
-
スタブ(stub):戻り値を固定する係
- 例)
Math.random()を 항상0.1にする🎲➡️0.1
- 例)
-
スパイ(spy):本物を覗き見して、呼ばれ方を記録する係👀
- 例)
fetch()が何回呼ばれたか見る📞
- 例)
-
モック(mock):スタブ+「こう呼ばれるべき」まで面倒を見る係🎭
- 例)「
fetch('/users')が1回呼ばれるべき」まで検証する
- 例)「
-
フェイク(fake):簡易な代用品(実装は動くけど本物じゃない)🧩
- 例)DBの代わりにメモリ配列で動く“偽リポジトリ”📦
Vitestでは基本 vi ヘルパーで揃います。vi.fn / vi.spyOn / vi.mock などが中心です。(Vitest)
2) どこをモックすべき?判断のコツ🧭😺
テストが不安定になる原因は、だいたいこの4つ👇
- 時間(
Date.now,new Date,setTimeout)⏰ - 乱数(
Math.random)🎲 - 通信(
fetch, HTTPクライアント)🌐 - DB/外部サービス(本物は遅い・壊れやすい)🧱
✅ 結論:“外の世界”は切り離す ❌ 逆に:自分の関数の中身までモックし始めると、テストが「仕様」じゃなく「実装」に縛られて死にます🪦😇
3) ハンズオン:最小コードで「怖くない」を体感🧪🧸
3-1) スタブ:乱数を固定してテストを安定化🎲➡️📌
例:Math.random() によって結果が変わる関数をテストしたい
実装例(src/lucky.ts)
export function isLucky(): boolean {
return Math.random() < 0.2
}
テスト例(src/lucky.test.ts)
import { describe, it, expect, vi } from 'vitest'
import { isLucky } from './lucky'
describe('isLucky', () => {
it('乱数を固定して true を検証する', () => {
vi.spyOn(Math, 'random').mockReturnValue(0.1) // 0.1 < 0.2 なので true
expect(isLucky()).toBe(true)
})
it('乱数を固定して false を検証する', () => {
vi.spyOn(Math, 'random').mockReturnValue(0.9) // 0.9 < 0.2 ではない
expect(isLucky()).toBe(false)
})
})
ポイント😊✨
vi.spyOn(対象, 'メソッド')は「本物を監視しつつ差し替え」できる万能選手💪- これだけでテストが「運ゲー」から卒業します🎓🎲🚫
3-2) フェイクタイマー:時間に依存する処理を爆速テスト⏰⚡
例:setTimeout があるとテストが遅い&不安定になりがち😵💫
Vitestは fake timers で時間を進められます。(Vitest)
実装例(src/after.ts)
export function executeAfter(ms: number, fn: () => void) {
setTimeout(fn, ms)
}
テスト例(src/after.test.ts)
import { describe, it, expect, vi } from 'vitest'
import { executeAfter } from './after'
describe('executeAfter', () => {
it('時間を進めてコールされることを確認', () => {
vi.useFakeTimers()
const fn = vi.fn()
executeAfter(1000, fn)
expect(fn).not.toHaveBeenCalled()
vi.advanceTimersByTime(1000) // 1秒進める
expect(fn).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
})
🔥 よくある注意
vi.useFakeTimers()したまま放置すると 他のテストが変になります😇 →afterEachで戻すクセをつけると安全🧯
3-3) 日時固定:new Date() が絡む処理を安定化📅🧊
「今日が何日か」で結果が変わる処理、テストで事故りがち!💥 Vitestは 日時も固定できます(fake timers の仕組みで制御)。(Vitest)
テスト例(ざっくり)
import { describe, it, expect, vi } from 'vitest'
function greeting() {
const hour = new Date().getHours()
return hour < 12 ? 'おはよう' : 'こんにちは'
}
describe('greeting', () => {
it('朝なら「おはよう」', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-02-10T09:00:00.000Z'))
expect(greeting()).toBe('おはよう')
vi.useRealTimers()
})
})
3-4) グローバルのスタブ:fetch を丸ごと差し替える🌐🧪
HTTPは本物を叩くと遅い・壊れやすい・通信失敗する…で地獄👹
Vitestは vi.stubGlobal で fetch 自体を差し替えできます。(Vitest)
例:fetch をスタブして JSON を返す
import { describe, it, expect, vi, afterEach } from 'vitest'
async function loadUserName() {
const res = await fetch('https://example.com/users/1')
const data = await res.json()
return data.name
}
describe('loadUserName', () => {
afterEach(() => {
vi.unstubAllGlobals() // これ超大事!🔥
})
it('fetchを差し替えて安定テスト', async () => {
vi.stubGlobal('fetch', vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'Taro' }),
} as any)
))
await expect(loadUserName()).resolves.toBe('Taro')
expect(fetch).toHaveBeenCalledTimes(1)
})
})
ポイント😊
vi.stubGlobalは デフォで自動リセットされないので、vi.unstubAllGlobals()が安全策です🧯(Vitest)
4) モジュールモック:外部モジュール丸ごと差し替える📦🎭
「APIクライアント」「DBアクセス層」みたいな 別ファイル を切り離す時に使います。
Vitestのモックは vi.mock() が基本。(Vitest)
例:userApi.ts を service.ts から呼んでる想定
// userApi.ts
export async function fetchUser() {
return { id: 1, name: 'Real' }
}
// service.ts
import { fetchUser } from './userApi'
export async function getUserName() {
const u = await fetchUser()
return u.name
}
テストで userApi を差し替え👇
import { describe, it, expect, vi } from 'vitest'
import { getUserName } from './service'
vi.mock('./userApi', () => {
return {
fetchUser: vi.fn(async () => ({ id: 1, name: 'Mocked' })),
}
})
describe('getUserName', () => {
it('モジュールを差し替えて検証', async () => {
await expect(getUserName()).resolves.toBe('Mocked')
})
})
⚠️ 罠:vi.mock() はファイル上部に巻き上げ(hoist)される性質があるので、配置で挙動が変わり得ます。(Vitest)
5) 初心者がやりがちな「やりすぎモック」回避術🛑😅
✅ “ちょうどいい”目安
- 外部境界だけモック(時間・乱数・通信・DB)🧱
- 自分のロジックは なるべく本物でテスト🧠✨
❌ ありがちな地雷
- 実装の細部(内部関数呼び出し順)まで
toHaveBeenCalledWithで縛る → リファクタでテストが死ぬ💀 - 1テストでモックが多すぎる → 何がしたいテストか分からない迷子テスト🐣🧭
- fake timers を戻し忘れて他テストが崩壊😇⏰
6) ミニ課題🎒✨(手を動かすと一気に定着するよ!)
- 🎲
Math.random()を固定して「当たり/はずれ」の2ケースを書く - ⏰
setTimeoutを fake timers で即時完了させるテストを書く - 🌐
fetchをvi.stubGlobalで差し替えて、JSONを返すテストを書く(最後にvi.unstubAllGlobals())
7) AI拡張で時短するコツ🤖✨(でも事故らない!)
AIに投げるときは “仕様”を渡すのがコツです😊 おすすめプロンプト例👇
- 🧪「この関数のテストケースを、外部依存(Date/Random/fetch)を固定する前提で3つ提案して」
- 🎭「Vitestで
vi.spyOnとvi.stubGlobalを使い分ける例を、このコードに合わせて書いて」 - 🧯「このテストが不安定になる可能性を指摘して。fake timers や unstub 忘れもチェックして」
仕上げにあなたがやることはこれだけ👇
- 期待値が“仕様”になってる?(実装に寄ってない?)🧠
- モックの後始末できてる?(
useRealTimers/unstubAllGlobals)🧹 - テスト名が「何を守るか」を説明してる?📛
まとめ🎉
モック/スタブは「ズル」じゃなくて、テストを安定させるための道具です🧰✨ でもやりすぎると逆に不安定になるので、まずはこの合言葉でいきましょ👇
**「外の世界だけ固定して、ロジックは本物で!」**🌍🔒🧠✅