sinon vs jest.mock vs vi.fn: Mocking in JavaScript Testing (2026)
TL;DR
sinon is a standalone mocking library — works with any test framework (Mocha, Jasmine, Node test runner), provides spies, stubs, mocks, and fake timers in one package. jest.mock is Jest's built-in module mocking system — auto-mocks entire modules, works at the module resolution level (not just function level). vi.fn / vi.mock is Vitest's equivalent — nearly identical API to Jest but faster, ESM-native, and integrates with Vite. In 2026: use vi.fn/vi.mock for Vitest projects (the new standard), jest.mock for Jest projects, sinon when you're using a non-Jest/Vitest framework or need standalone mocking.
Key Takeaways
- sinon: ~3M weekly downloads — framework-agnostic, standalone spies/stubs/mocks/fakes
- jest-mock (jest.mock): bundled with Jest — module-level mocking,
jest.fn(),jest.spyOn() - vitest (vi.fn): bundled with Vitest — Jest-compatible API, ESM-native, faster HMR
- sinon works with any test runner; jest.mock / vi.mock require Jest / Vitest respectively
jest.mock/vi.mockare hoisted to the top of the file — order doesn't matter- Fake timers (sinon/jest/vi) replace
setTimeout,Datefor deterministic time-based tests
Core Concepts
Spy → wraps a real function, records calls, doesn't change behavior
Stub → replaces a function with a configurable fake, controls return values
Mock → strict spy/stub with built-in assertion expectations
Fake → complete replacement (fake timers, fake XHR)
Module mock → replaces an entire module (all exports) at resolution time
sinon
sinon — standalone test doubles library:
Spies
import sinon from "sinon"
import { describe, it, before, after } from "mocha"
import assert from "node:assert"
describe("PackageService", () => {
it("calls npm API once per package", async () => {
const service = new PackageService()
// Spy wraps the real method — still executes:
const spy = sinon.spy(service, "fetchFromNpm")
await service.getHealthScore("react")
// Assert on call behavior:
assert.ok(spy.calledOnce)
assert.ok(spy.calledWith("react"))
assert.ok(spy.returned(sinon.match.object)) // Return value was an object
spy.restore() // Restore original method
})
})
Stubs
import sinon from "sinon"
describe("HealthScoreCalculator", () => {
let npmStub: sinon.SinonStub
let githubStub: sinon.SinonStub
before(() => {
// Replace real API calls with controlled fakes:
npmStub = sinon.stub(npmClient, "getDownloads").resolves({
weekly: 5_000_000,
monthly: 20_000_000,
})
githubStub = sinon.stub(githubClient, "getRepoStats").resolves({
stars: 224_000,
openIssues: 847,
lastCommit: new Date("2026-01-15"),
})
})
after(() => {
npmStub.restore()
githubStub.restore()
})
it("calculates score from download and star data", async () => {
const score = await calculator.compute("react")
assert.strictEqual(score, 92.5)
// Assert stub was called:
assert.ok(npmStub.calledOnceWith("react"))
assert.ok(githubStub.calledOnceWith("facebook", "react"))
})
it("returns 0 on npm API error", async () => {
npmStub.rejects(new Error("npm API down"))
const score = await calculator.compute("react")
assert.strictEqual(score, 0)
})
})
Stub return value patterns
const stub = sinon.stub()
// Always return a value:
stub.returns(42)
stub.resolves({ data: "ok" }) // For async
// Return different values on consecutive calls:
stub.onFirstCall().returns(1)
stub.onSecondCall().returns(2)
stub.onThirdCall().throws(new Error("rate limit"))
// Return based on arguments:
stub.withArgs("react").returns({ score: 92 })
stub.withArgs("vue").returns({ score: 89 })
stub.withArgs(sinon.match.string).returns({ score: 50 }) // Default
// Yield to callback (for callback-style APIs):
stub.yields(null, { data: "result" }) // callback(null, data)
stub.callsFake((name, cb) => cb(null, { name }))
Fake timers
import sinon from "sinon"
describe("RateLimiter", () => {
let clock: sinon.SinonFakeTimers
before(() => {
// Replace setTimeout, setInterval, Date with fakes:
clock = sinon.useFakeTimers(new Date("2026-01-01T00:00:00Z"))
})
after(() => {
clock.restore()
})
it("resets window after 1 minute", async () => {
const limiter = new RateLimiter({ windowMs: 60_000, max: 100 })
// Exhaust the limit:
for (let i = 0; i < 100; i++) {
limiter.hit("user-123")
}
assert.ok(limiter.isLimited("user-123"))
// Fast-forward 60 seconds:
clock.tick(60_000)
// Window reset:
assert.ok(!limiter.isLimited("user-123"))
})
})
Sandbox (recommended for cleanup)
import sinon from "sinon"
describe("with sandbox", () => {
const sandbox = sinon.createSandbox()
afterEach(() => {
// Restore ALL stubs/spies created in this sandbox at once:
sandbox.restore()
})
it("stubs database", () => {
const dbStub = sandbox.stub(db, "query").resolves([{ id: 1 }])
const cacheStub = sandbox.stub(cache, "get").returns(null)
// No need to call restore() individually — sandbox handles it
})
})
jest.mock / jest.fn
Jest — built-in mocking:
jest.fn() — mock a function
import { describe, it, expect, jest } from "@jest/globals"
describe("PackageService", () => {
it("calls the fetch function", async () => {
// Create a mock function:
const mockFetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue({ downloads: 5_000_000 }),
})
const service = new PackageService(mockFetch)
await service.getDownloads("react")
// Built-in matchers:
expect(mockFetch).toHaveBeenCalledOnce()
expect(mockFetch).toHaveBeenCalledWith(
"https://api.npmjs.org/downloads/point/last-week/react"
)
})
})
jest.mock() — auto-mock a module
// jest.mock is HOISTED — works even if called after imports:
jest.mock("../src/npmClient")
jest.mock("../src/githubClient", () => ({
getRepoStats: jest.fn().mockResolvedValue({ stars: 224_000 }),
}))
import { getDownloads } from "../src/npmClient"
import { getRepoStats } from "../src/githubClient"
import { HealthScoreCalculator } from "../src/calculator"
describe("HealthScoreCalculator", () => {
beforeEach(() => {
jest.clearAllMocks()
// Type assertion needed after jest.mock:
;(getDownloads as jest.Mock).mockResolvedValue({ weekly: 5_000_000 })
})
it("computes score from npm and github data", async () => {
const score = await HealthScoreCalculator.compute("react")
expect(score).toBe(92.5)
expect(getDownloads).toHaveBeenCalledWith("react")
expect(getRepoStats).toHaveBeenCalledWith("facebook", "react")
})
})
jest.spyOn() — spy on real methods
import * as fs from "node:fs"
describe("ConfigLoader", () => {
it("reads the config file", () => {
// Spy wraps the real function (still executes by default):
const readSpy = jest.spyOn(fs, "readFileSync").mockReturnValue(
JSON.stringify({ theme: "dark" })
)
const config = ConfigLoader.load("./.config.json")
expect(readSpy).toHaveBeenCalledWith("./.config.json", "utf-8")
expect(config.theme).toBe("dark")
readSpy.mockRestore()
})
})
Fake timers
describe("Debouncer", () => {
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
})
it("debounces rapidly fired calls", () => {
const callback = jest.fn()
const debounced = debounce(callback, 300)
debounced()
debounced()
debounced()
expect(callback).not.toHaveBeenCalled()
jest.advanceTimersByTime(300)
// Only called once after the debounce delay:
expect(callback).toHaveBeenCalledOnce()
})
})
vi.fn / vi.mock (Vitest)
Vitest — Jest-compatible API with Vite:
vi.fn() — identical to jest.fn()
import { describe, it, expect, vi, beforeEach } from "vitest"
describe("PackageService", () => {
it("fetches package data", async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({ name: "react", version: "19.0.0" }),
})
globalThis.fetch = mockFetch
const data = await PackageService.fetch("react")
expect(mockFetch).toHaveBeenCalledOnce()
expect(data.name).toBe("react")
})
})
vi.mock() — module mocking
import { describe, it, expect, vi, beforeEach } from "vitest"
// Auto-mock: replaces all exports with vi.fn():
vi.mock("../src/database")
// Factory function: define specific mock implementations:
vi.mock("../src/npmClient", () => ({
getNpmData: vi.fn(),
getDownloads: vi.fn(),
}))
import { getNpmData } from "../src/npmClient"
import { db } from "../src/database"
describe("PackageRepository", () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(getNpmData).mockResolvedValue({ downloads: 5_000_000 })
vi.mocked(db.query).mockResolvedValue([{ id: 1, name: "react" }])
})
it("combines npm and db data", async () => {
const result = await PackageRepository.getEnriched("react")
expect(result.downloads).toBe(5_000_000)
expect(getNpmData).toHaveBeenCalledWith("react")
})
})
vi.importActual — partial mocking
// Mock only specific exports, keep the rest real:
vi.mock("../src/utils", async (importActual) => {
const actual = await importActual<typeof import("../src/utils")>()
return {
...actual,
// Only mock this one function:
fetchWithRetry: vi.fn().mockResolvedValue({ data: "mocked" }),
}
})
Fake timers
import { vi } from "vitest"
describe("CacheExpiry", () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"))
})
afterEach(() => {
vi.useRealTimers()
})
it("expires cache after TTL", () => {
const cache = new Cache({ ttl: 60_000 })
cache.set("key", "value")
vi.advanceTimersByTime(61_000)
expect(cache.get("key")).toBeUndefined()
})
})
Feature Comparison
| Feature | sinon | jest.fn/mock | vi.fn/mock |
|---|---|---|---|
| Framework | Any (Mocha, node:test) | Jest only | Vitest only |
| Module mocking | ❌ (function-level only) | ✅ | ✅ |
| Spy / Stub / Mock | ✅ Full suite | ✅ (jest.fn, spyOn) | ✅ (vi.fn, spyOn) |
| Fake timers | ✅ | ✅ | ✅ |
| Fake XHR / server | ✅ (sinon-fakeserver) | ❌ | ❌ |
| ESM support | ✅ | ⚠️ (requires config) | ✅ Native |
| TypeScript types | ✅ | ✅ | ✅ |
| Auto-reset between tests | Manual (sandbox) | clearAllMocks | clearAllMocks |
| Weekly downloads | ~3M | bundled with Jest | bundled with Vitest |
When to Use Each
Choose sinon if:
- Using Mocha, node:test, or any non-Jest/Vitest framework
- Need fake XMLHttpRequest or fetch server for browser testing
- Migrating legacy Mocha test suites
- Want framework-agnostic mocking that works everywhere
Choose jest.fn / jest.mock if:
- Already using Jest (CRA projects, older Next.js setups)
- Need
jest.mock()hoisting to mock complex module graphs - Team is familiar with Jest's assertion API
Choose vi.fn / vi.mock if:
- Using Vitest (the recommended choice for new projects in 2026)
- Same API as Jest but faster, native ESM, works with Vite
- Building a Vite/SvelteKit/Nuxt/Astro project
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on sinon v18.x, jest v29.x, and vitest v3.x.
Compare testing libraries and developer tooling on PkgPulse →