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

第23章 テスト②:Spyで“呼ばれ方”を確認する🕵️‍♀️📞

「結果(return値)だけじゃなくて、**“何が何回・どんな引数で呼ばれたか”**も確認したい…!」 そんな時に使うのが Spy(スパイ) だよ〜🕵️‍♀️💕


この章のゴール🎯🌸

  • ✅ 「Spyってなに?」を、友達に説明できる
  • ✅ logger / api みたいな 外部とのやりとりの呼ばれ方をテストできる
  • ✅ Spyを使いすぎてテストが壊れやすくなるのを避けられる🚧

1) Spy(スパイ)ってなに?🕵️‍♀️

✨(ざっくり超イメージ)

Spyは「関数の呼び出しをのぞき見して、メモる子」だよ📒👀

  • 何回呼ばれた?(回数)
  • どんな引数で呼ばれた?(中身)
  • どの順番で呼ばれた?(順序)

Jestのドキュメントでも、モック関数は “spies” として扱えるよ〜って説明されてるよ。(Jest)


2) Spy / Mock / Fake / Stub の違い(迷子防止🧭)

名前役割イメージ
Fakeそれっぽく動く「代用品」InMemory DB、FakeClock⏰
Stub決まった値だけ返す「置物」getRate() が常に 110 を返す💴
Spy呼ばれ方を記録する「監視員」loggerが何回呼ばれたか📞
Mock期待する呼ばれ方を決めて検証する(Spy+期待)「1回だけ呼べ!」みたいな🎯

この章は Spy(監視して記録) が主役だよ🕵️‍♀️💕


3) まずは “ライブラリ無し” でSpyを作って理解しよ🧠✨

「仕組みがわかる」と、Vitest/Jest使う時も怖くなくなる😊

✅ 自作Spy(最小)

export type SpyFn<Args extends any[] = any[], Ret = any> = ((...args: Args) => Ret) & {
calls: Args[];
};

export function createSpy<Args extends any[] = any[], Ret = any>(
impl?: (...args: Args) => Ret
): SpyFn<Args, Ret> {
const fn = ((...args: Args) => {
fn.calls.push(args);
return impl?.(...args) as Ret;
}) as SpyFn<Args, Ret>;

fn.calls = [];
return fn;
}

4) DIと相性バツグンな題材:loggerの呼ばれ方をテスト📣✨

実装(依存は外から渡す)💉

export type Logger = {
info: (message: string) => void;
warn: (message: string) => void;
};

export type UserRepo = {
save: (name: string) => Promise<{ id: string }>;
};

export function makeRegisterUser(deps: { repo: UserRepo; logger: Logger }) {
return async function registerUser(name: string) {
const saved = await deps.repo.save(name);

deps.logger.info(`user created: ${saved.id}`); // ←ここをSpyで見たい🕵️‍♀️

return saved.id;
};
}

テスト(自作Spyで確認🕵️‍♀️)

import { createSpy } from "./spy";
import { makeRegisterUser } from "./registerUser";

test("logger.info が正しい文字列で1回呼ばれる", async () => {
const repo = {
save: async () => ({ id: "U-001" }),
};

const infoSpy = createSpy<[string], void>();
const logger = {
info: infoSpy,
warn: createSpy<[string], void>(),
};

const registerUser = makeRegisterUser({ repo, logger });
await registerUser("komi");

expect(infoSpy.calls.length).toBe(1);
expect(infoSpy.calls[0][0]).toBe("user created: U-001");
});

👉 これで「loggerが呼ばれたこと」を 結果じゃなく “やりとり” として検証できたね📞✨


5) 実戦:VitestでSpy(いちばんよく見る形🧪⚡)

Vitestは vi.fnvi.spyOn を用意してて、モック/スパイ周りのガイドもあるよ。(vitest.dev)

5-1) vi.fn():Spyとして使う(超定番)🕵️‍♀️

import { describe, test, expect, vi } from "vitest";
import { makeRegisterUser } from "./registerUser";

describe("registerUser", () => {
test("logger.info が正しく呼ばれる", async () => {
const repo = { save: async () => ({ id: "U-001" }) };

const logger = {
info: vi.fn(),
warn: vi.fn(),
};

const registerUser = makeRegisterUser({ repo, logger });
await registerUser("komi");

expect(logger.info).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith("user created: U-001");
});
});

5-2) vi.spyOn():既存メソッドを“のぞき見”する👀

「本物の実装はそのまま、呼ばれ方だけ見たい」時に便利だよ。(Zenn)

import { test, expect, vi } from "vitest";

test("spyOnでconsole.logの呼ばれ方を見る", () => {
const spy = vi.spyOn(console, "log");

console.log("hello");

expect(spy).toHaveBeenCalledWith("hello");

spy.mockRestore(); // 元に戻すの大事⚠️
});

✅ 片付け(超大事🧹)

Spyは「のぞき見状態」が残ると次のテストに悪影響💥 Vitestは restore/clear/reset 系を用意していて、spyOnしたものを元に戻す vi.restoreAllMocks() もあるよ。(vitest.dev)


6) Jestでも同じ考え方でOK🧪🟩

Jestも jest.fn() / jest.spyOn() が基本セットだよ。(Jest)

import { test, expect, jest } from "@jest/globals";
import { makeRegisterUser } from "./registerUser";

test("logger.info が呼ばれる(Jest)", async () => {
const repo = { save: async () => ({ id: "U-001" }) };
const logger = { info: jest.fn(), warn: jest.fn() };

const registerUser = makeRegisterUser({ repo, logger });
await registerUser("komi");

expect(logger.info).toHaveBeenCalledWith("user created: U-001");
});

7) Node標準の node:test でもSpyできるよ🧪🟦

Nodeの node:testmock で spy 的なことができる(公式ドキュメントに例があるよ)📚✨ (Node.js)


8) Spyテストの「使いどころ」3ルール📏💗(壊れにくさ重視)

  1. 外部境界だけ見る(logger / http / db / storage など)🚪
  2. ✅ まずは「結果」でテストできないか考える(結果で足りるならSpy不要)🍀
  3. ✅ 期待が細かすぎると壊れやすい(ログ文言の完全一致地獄⚠️)

9) ミニ課題🧩✨(今日の手を動かすコーナー)

課題A:回数テスト📞

  • logger.info が 1回だけ 呼ばれることをテストしよう✅

課題B:引数テスト📝

  • "user created: <id>"<id> が正しいことを確認しよう✅

課題C:順序テスト(余裕あれば)🧠

  • repo.save() のあとに logger.info() が呼ばれてる、を確認してみよう✨ (順序は壊れやすいから“本当に必要な時だけ”でOK🥺)

10) AIに手伝ってもらうプロンプト例🤖💬

  • 「この関数のテストで、Spyで確認すべき呼び出しはどこ?理由もつけて」
  • 「Vitestで toHaveBeenCalledWith を使ったテストを書いて。壊れにくい期待にして」
  • 「ログ文言を完全一致じゃなくて “重要部分だけ” 検証する案を3つ出して」

次の章(第24章)では、「どれをMock/Spyにして、どれをFakeにするのが健全か」っていう境界のテスト方針🎯に入っていくよ〜!🧪🌿