Sinon.js の使い方 — JavaScript テストのスパイ・スタブ・モックを完全攻略
一言でいうと
Sinon.js は、JavaScript のテストにおいて スパイ(spy)・スタブ(stub)・モック(mock)・フェイクタイマー などのテストダブルを提供するライブラリです。特定のテストフレームワークに依存せず、Mocha・Jest・Vitest など任意のフレームワークと組み合わせて使えます。
どんな時に使う?
- 外部APIやDB呼び出しをテストから切り離したい — HTTP リクエストやデータベースアクセスをスタブに差し替え、高速かつ安定したテストを実現する
- 関数が正しい引数・回数で呼ばれたか検証したい — コールバックやイベントハンドラの呼び出しをスパイで監視し、アサーションする
setTimeoutやDate.nowに依存するロジックをテストしたい — フェイクタイマーで時間を自在にコントロールし、待ち時間なしでテストする
インストール
# npm
npm install --save-dev sinon
# yarn
yarn add --dev sinon
# pnpm
pnpm add --save-dev sinon
TypeScript で使う場合は型定義も追加します。
npm install --save-dev @types/sinon
基本的な使い方
最もよく使うパターンとして、オブジェクトのメソッドをスタブに差し替えてテストする例を示します。
import sinon from "sinon";
import assert from "node:assert";
// テスト対象の依存オブジェクト
const userRepository = {
findById(id: string): { id: string; name: string } | null {
// 実際にはDBアクセスが発生する
throw new Error("DB connection required");
},
};
// テスト対象の関数
function getUserName(id: string): string {
const user = userRepository.findById(id);
return user ? user.name : "Unknown";
}
// --- テストコード ---
describe("getUserName", () => {
afterEach(() => {
// すべてのスタブ・スパイを元に戻す
sinon.restore();
});
it("ユーザーが見つかった場合、名前を返す", () => {
// findById をスタブに差し替え
sinon.stub(userRepository, "findById").returns({ id: "1", name: "太郎" });
const result = getUserName("1");
assert.strictEqual(result, "太郎");
assert.ok((userRepository.findById as sinon.SinonStub).calledOnceWith("1"));
});
it("ユーザーが見つからない場合、'Unknown' を返す", () => {
sinon.stub(userRepository, "findById").returns(null);
const result = getUserName("999");
assert.strictEqual(result, "Unknown");
});
});
よく使う API
1. sinon.spy() — 関数の呼び出しを監視する
スパイは元の関数の動作を変えずに、呼び出し情報を記録します。
import sinon from "sinon";
// 単独のスパイ(コールバック監視に便利)
const callback = sinon.spy();
callback("hello", 42);
console.log(callback.called); // true
console.log(callback.calledOnce); // true
console.log(callback.firstCall.args); // ["hello", 42]
// 既存メソッドをラップするスパイ
const obj = {
greet(name: string) {
return `Hello, ${name}`;
},
};
const spy = sinon.spy(obj, "greet");
obj.greet("World"); // 元の実装がそのまま動く
console.log(spy.calledWith("World")); // true
console.log(spy.returnValues[0]); // "Hello, World"
spy.restore(); // 元に戻す
2. sinon.stub() — 関数の動作を差し替える
スタブはスパイの機能に加え、戻り値や例外を自由に制御できます。
import sinon from "sinon";
const api = {
fetchData(endpoint: string): Promise<string> {
return fetch(endpoint).then((res) => res.text());
},
};
// 基本的なスタブ
const stub = sinon.stub(api, "fetchData");
// 引数に応じた戻り値の設定
stub.withArgs("/users").resolves('{"users":[]}');
stub.withArgs("/posts").resolves('{"posts":[]}');
stub.rejects(new Error("Not Found")); // デフォルト
// 使用
async function test() {
console.log(await api.fetchData("/users")); // '{"users":[]}'
console.log(await api.fetchData("/posts")); // '{"posts":[]}'
try {
await api.fetchData("/unknown");
} catch (e) {
console.log((e as Error).message); // "Not Found"
}
}
// 呼び出し順序に応じた戻り値
const seqStub = sinon.stub();
seqStub.onFirstCall().returns("1回目");
seqStub.onSecondCall().returns("2回目");
seqStub.returns("3回目以降");
console.log(seqStub()); // "1回目"
console.log(seqStub()); // "2回目"
console.log(seqStub()); // "3回目以降"
stub.restore();
3. sinon.mock() — 期待値を事前に定義して検証する
モックはスタブ+期待値の検証を一体化したものです。「このメソッドがこの引数で N 回呼ばれるはず」を事前に宣言し、最後に verify() で検証します。
import sinon from "sinon";
const notifier = {
send(to: string, message: string): boolean {
// 実際にはメール送信
return true;
},
};
const mock = sinon.mock(notifier);
// 期待値の定義
mock
.expects("send")
.once() // 1回だけ呼ばれること
.withArgs("admin@example.com", sinon.match.string) // 第1引数が一致すること
.returns(true);
// テスト対象のコードを実行
notifier.send("admin@example.com", "サーバー障害発生");
// 期待値の検証(条件を満たさなければ例外がスローされる)
mock.verify();
mock.restore();
4. sinon.useFakeTimers() — タイマーと時刻を制御する
setTimeout、setInterval、Date.now() などを完全にコントロールできます。
import sinon from "sinon";
import assert from "node:assert";
function delayedGreeting(callback: (msg: string) => void): void {
setTimeout(() => {
callback(`Hello at ${new Date().toISOString()}`);
}, 5000);
}
describe("delayedGreeting", () => {
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
// 2025年1月1日 00:00:00 UTC に固定
clock = sinon.useFakeTimers(new Date("2025-01-01T00:00:00Z").getTime());
});
afterEach(() => {
clock.restore();
});
it("5秒後にコールバックが呼ばれる", () => {
const spy = sinon.spy();
delayedGreeting(spy);
// まだ呼ばれていない
assert.ok(spy.notCalled);
// 5秒進める
clock.tick(5000);
// 呼ばれた
assert.ok(spy.calledOnce);
assert.ok(spy.firstCall.args[0].includes("2025-01-01T00:00:05"));
});
});
5. sinon.fake() — シンプルなフェイク関数を作る
sinon.fake は spy と stub の機能を統合した、よりシンプルな API です。
import sinon from "sinon";
// 固定値を返すフェイク
const fakeReturn = sinon.fake.returns(42);
console.log(fakeReturn()); // 42
// Promise を解決するフェイク
const fakeResolves = sinon.fake.resolves("done");
await fakeResolves(); // "done"
// 例外をスローするフェイク
const fakeThrows = sinon.fake.throws(new Error("boom"));
try {
fakeThrows();
} catch (e) {
console.log((e as Error).message); // "boom"
}
// カスタム実装のフェイク
const fakeCustom = sinon.fake((x: number) => x * 2);
console.log(fakeCustom(5)); // 10
console.log(fakeCustom.callCount); // 1
// 既存メソッドをフェイクに差し替え
const obj = { method: () => "original" };
sinon.replace(obj, "method", sinon.fake.returns("replaced"));
console.log(obj.method()); // "replaced"
sinon.restore();
類似パッケージとの比較
| 特徴 | Sinon.js | Jest (組み込み) | Vitest (組み込み) | testdouble.js |
|---|---|---|---|---|
| フレームワーク依存 | なし(どれとでも使える) | Jest 専用 | Vitest 専用 | なし |
| スパイ | ✅ sinon.spy() | ✅ jest.fn() | ✅ vi.fn() | ✅ td.function() |
| スタブ | ✅ sinon.stub() | ✅ jest.spyOn() | ✅ vi.spyOn() | ✅ td.when() |
| モック | ✅ sinon.mock() | ✅ jest.mock() | ✅ vi.mock() | ✅ |
| フェイクタイマー | ✅ 内蔵 | ✅ 内蔵 | ✅ 内蔵 | ❌ 別途必要 |
| フェイク XMLHttpRequest | ✅ 内蔵 | ❌ | ❌ | ❌ |
| サンドボックス | ✅ 自動クリーンアップ | ✅ 自動 | ✅ 自動 | ✅ td.reset() |
| 学習コスト | 中(API が豊富) | 低(Jest ユーザーなら) | 低(Vitest ユーザーなら) | 低 |
選定の指針: Jest や Vitest を使っているなら組み込みのモック機能で十分なケースが多いです。Sinon.js は フレームワークに依存しない汎用性、きめ細かい引数マッチング、フェイク XHR/サーバー が必要な場合に強みを発揮します。
注意点・Tips
🔴 sinon.restore() を必ず呼ぶ
テスト間でスタブが残ると、他のテストに影響します。afterEach で必ず sinon.restore() を呼びましょう。
afterEach(() => {
sinon.restore(); // すべての spy/stub/mock/fake timer を一括リストア
});
🔴 サンドボックスを活用する
sinon のトップレベルオブジェクト自体がデフォルトサンドボックスとして機能します(v5 以降)。個別にサンドボックスを作りたい場合は sinon.createSandbox() を使います。
const sandbox = sinon.createSandbox();
sandbox.stub(obj, "method").returns("stubbed");
// テスト後
sandbox.restore(); // このサンドボックスで作ったものだけリストア
🔴 stub.callsFake() と stub.returns() の使い分け
returns(value)— 固定値を返すだけcallsFake(fn)— 引数に応じた動的な振る舞いが必要な場合
const stub = sinon.stub();
stub.callsFake((id: number) => {
if (id === 1) return { name: "太郎" };
return null;
});
🔴 sinon.assert で可読性の高いアサーション
Sinon 組み込みのアサーションは、失敗時のメッセージが分かりやすいです。
sinon.assert.calledOnce(stub);
sinon.assert.calledWith(stub, "expected-arg");
sinon.assert.callOrder(stub1, stub2); // 呼び出し順序の検証
🔴 ES Modules 環境での注意
sinon.stub(obj, "method") はオブジェクトのプロパティを書き換える仕組みのため、ES Modules の名前付きエクスポートを直接スタブ化することはできません。依存注入パターンを使うか、モジュール全体をオブジェクトとしてインポートする設計にしましょう。
// ❌ これは動かない
import { fetchUser } from "./userService";
sinon.stub(???, "fetchUser"); // スタブ化する対象オブジェクトがない
// ✅ オブジェクトとしてインポート
import * as userService from "./userService";
sinon.stub(userService, "fetchUser").resolves({ id: "1", name: "太郎" });
// ※ ただしバンドラーや実行環境によ