jest-mock, sinon, and testdouble are JavaScript libraries designed to help developers create isolated unit tests by replacing real dependencies with controllable substitutes. These tools enable mocking functions, modules, timers, and network requests to verify behavior without side effects. While all three serve the core purpose of test isolation, they differ significantly in scope, API design, integration capabilities, and philosophical approach to testing.
When writing unit tests in JavaScript, you often need to isolate the code under test from its dependencies — whether those are external APIs, database calls, or other modules. That’s where test doubles come in. jest-mock, sinon, and testdouble each offer ways to replace real functions with controlled substitutes, but they do so with different philosophies, APIs, and trade-offs. Let’s break down how they compare in real-world usage.
All three libraries let you create spies (to observe calls), stubs (to replace behavior), and mocks (to both observe and control). But their scope and focus vary.
jest-mock: Lightweight and Jest-Tightjest-mock is the standalone version of Jest’s internal mocking utilities. It’s not meant to be used independently in most cases — it’s primarily bundled with Jest itself. If you’re using Jest, you already have access to these features via jest.fn(), jest.spyOn(), and module mocking through jest.mock().
// Using jest-mock via Jest's global API
const mockFn = jest.fn();
mockFn.mockReturnValue('hello');
expect(mockFn()).toBe('hello');
expect(mockFn).toHaveBeenCalled();
It doesn’t include fake timers or XHR mocking — those are separate parts of Jest (jest.useFakeTimers(), fetch mocking via manual setup).
sinon: The Swiss Army Knifesinon is a full-featured, framework-agnostic test double library. It provides spies, stubs, mocks, fake timers, and even fake XHR/fetch implementations.
// Sinon example
import sinon from 'sinon';
const stub = sinon.stub();
stub.returns('hello');
expect(stub()).toBe('hello');
expect(stub.calledOnce).toBe(true);
// Fake timers
const clock = sinon.useFakeTimers();
setTimeout(() => {}, 1000);
clock.tick(1000);
clock.restore();
You can use it with any test runner — Jest, Mocha, Jasmine, etc.
testdouble: Opinionated and Minimalisttestdouble (often abbreviated as td) takes a different approach. It avoids the term “mock” in favor of “test double” and discourages over-specification. It focuses on verifying what your code does rather than how it does it.
// testdouble example
import td from 'testdouble';
const fakeFn = td.function();
td.when(fakeFn()).thenReturn('hello');
expect(fakeFn()).toBe('hello');
// Verification is explicit and descriptive
td.verify(fakeFn());
It intentionally omits features like fake timers or XHR fakes, encouraging you to design your code so those concerns are abstracted behind interfaces you can easily replace.
Let’s compare how each handles the most common task: replacing a function’s behavior.
jest-mock:
const fn = jest.fn().mockReturnValue('ok');
// Or with conditional returns
fn.mockImplementation(arg => arg === 'test' ? 'yes' : 'no');
sinon:
const fn = sinon.stub().returns('ok');
// Or with conditional logic
fn.callsFake(arg => arg === 'test' ? 'yes' : 'no');
testdouble:
const fn = td.function();
td.when(fn()).thenReturn('ok');
// Conditional behavior
td.when(fn('test')).thenReturn('yes');
td.when(fn('other')).thenReturn('no');
💡 Note:
testdoubleuses when/then syntax, which reads like a specification: “when called with X, then return Y.” This makes test intent clearer but requires more setup lines.
Sometimes you don’t want to replace a function entirely — just observe how it’s called.
jest-mock:
const original = console.log;
const spy = jest.spyOn(console, 'log');
console.log('hi');
expect(spy).toHaveBeenCalledWith('hi');
spy.mockRestore(); // restore original
sinon:
const spy = sinon.spy(console, 'log');
console.log('hi');
expect(spy.calledWith('hi')).toBe(true);
spy.restore();
testdouble:
// testdouble doesn't support spying on existing objects directly.
// Instead, it encourages dependency injection:
function greet(logger) {
logger('hi');
}
const fakeLogger = td.function();
greet(fakeLogger);
td.verify(fakeLogger('hi'));
This highlights a key philosophical difference: testdouble pushes you to write code that accepts dependencies as arguments, making spying unnecessary.
Need to test setTimeout, setInterval, or Date?
jest-mock: Not included. Use Jest’s built-in jest.useFakeTimers().sinon: Built-in via sinon.useFakeTimers().testdouble: Not supported. You’re expected to wrap time-related logic in a service you can replace.Example with Sinon:
const clock = sinon.useFakeTimers();
let called = false;
setTimeout(() => called = true, 1000);
clock.tick(1000);
expect(called).toBe(true);
clock.restore();
If you rely heavily on timer mocking, sinon or Jest’s native tools are better choices.
How do you replace entire modules or classes?
jest-mock shines here with jest.mock():
// __mocks__/api.js
export const fetchData = () => Promise.resolve({ data: 'mock' });
// In test
jest.mock('./api');
sinon doesn’t handle module mocking — you’d typically use your test runner’s features or manual DI.
testdouble provides td.replace() for Node.js (via quibble) but it’s less seamless in browser environments:
// Node.js only
const api = td.replace('./api');
td.when(api.fetchData()).thenReturn(Promise.resolve({ data: 'mock' }));
For frontend apps using ES modules, jest-mock (via Jest) offers the smoothest experience for module-level mocking.
This is where the biggest differences lie:
jest-mock is pragmatic and integrated. It gets the job done with minimal fuss if you’re in the Jest ecosystem.sinon is powerful and flexible. It gives you every tool you might need, even if you end up misusing them.testdouble is opinionated and educational. It actively tries to prevent bad testing habits by limiting what you can do.For example, testdouble throws an error if you try to verify a call that wasn’t previously stubbed — this prevents “testing the mock” instead of real behavior. sinon and jest-mock let you do this freely.
Tests must be isolated, so cleanup is critical.
jest-mock:
beforeEach(() => {
jest.clearAllMocks(); // clears call history
// or jest.resetAllMocks() to also reset implementations
});
sinon:
let sandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
testdouble:
afterEach(() => {
td.reset(); // wipes all test doubles
});
All three handle cleanup well, but testdouble’s single td.reset() is the simplest.
jest-mock if you’re not using Jest — it’s not designed as a standalone solution.sinon if you want minimal dependencies or prefer a more guided testing style — its flexibility can lead to over-mocking.testdouble if you need fake timers, XHR fakes, or deep integration with module systems — it intentionally omits these.| Feature | jest-mock | sinon | testdouble |
|---|---|---|---|
| Test runner agnostic | ❌ (Jest-only) | ✅ | ✅ (mostly) |
| Spies | ✅ | ✅ | ✅ (via DI) |
| Stubs/Mocks | ✅ | ✅ | ✅ |
| Fake timers | ❌ (use Jest’s) | ✅ | ❌ |
| Module mocking | ✅ (excellent) | ❌ | ⚠️ (Node.js only) |
| Cleanup | jest.clearAllMocks() | sandbox.restore() | td.reset() |
| Philosophy | Pragmatic | Flexible | Opinionated / TDD-focused |
jest-mock if you’re in a Jest project and your mocking needs are straightforward. Don’t add extra deps if you don’t need them.sinon when you need maximum control, work across multiple test runners, or require advanced features like fake timers and XHR.testdouble if your team values clean test design, wants to avoid mocking pitfalls, and is willing to structure code around dependency injection.In practice, many teams never need to look beyond Jest’s built-in tools. But for complex applications or non-Jest environments, sinon remains the most versatile option — while testdouble serves as a thoughtful alternative for teams serious about test quality.
Choose jest-mock if you're already using Jest as your test runner and want lightweight, built-in mocking capabilities without adding extra dependencies. It's ideal for projects that rely on Jest's ecosystem and need basic function and module mocking with minimal configuration. However, avoid it if you require advanced features like complex stubbing behaviors or cross-test-runner compatibility.
Choose sinon if you need a mature, feature-rich mocking library that works across any test framework (Jest, Mocha, Jasmine, etc.) and supports spies, stubs, mocks, fake timers, and XHR fakes out of the box. It's well-suited for complex testing scenarios requiring fine-grained control over test doubles, but be prepared for a steeper learning curve and more verbose APIs compared to simpler alternatives.
Choose testdouble if you prioritize clean, readable test code and follow strict test-driven development practices that emphasize verifying behavior over implementation details. Its philosophy discourages over-mocking and encourages designing testable code from the start. It's best for teams committed to TDD principles who want a focused, opinionated tool that reduces boilerplate while promoting good testing habits.
Note: More details on user side API can be found in Jest documentation.
import {ModuleMocker} from 'jest-mock';
constructor(global)Creates a new module mocker that generates mocks as if they were created in an environment with the given global object.
generateFromMetadata(metadata)Generates a mock based on the given metadata (Metadata for the mock in the schema returned by the getMetadata() method of this module). Mocks treat functions specially, and all mock functions have additional members, described in the documentation for fn() in this module.
One important note: function prototypes are handled specially by this mocking framework. For functions with prototypes, when called as a constructor, the mock will install mocked function members on the instance. This allows different instances of the same constructor to have different values for its mocks member and its return values.
getMetadata(component)Inspects the argument and returns its schema in the following recursive format:
{
type: ...
members: {}
}
Where type is one of array, object, function, or ref, and members is an optional dictionary where the keys are member names and the values are metadata objects. Function prototypes are defined by defining metadata for the member.prototype of the function. The type of a function prototype should always be object. For instance, a class might be defined like this:
const classDef = {
type: 'function',
members: {
staticMethod: {type: 'function'},
prototype: {
type: 'object',
members: {
instanceMethod: {type: 'function'},
},
},
},
};
Metadata may also contain references to other objects defined within the same metadata object. The metadata for the referent must be marked with refID key and an arbitrary value. The referrer must be marked with a ref key that has the same value as object with refID that it refers to. For instance, this metadata blob:
const refID = {
type: 'object',
refID: 1,
members: {
self: {ref: 1},
},
};
Defines an object with a slot named self that refers back to the object.
fn(implementation?)Generates a stand-alone function with members that help drive unit tests or confirm expectations. Specifically, functions returned by this method have the following members:
.mockAn object with three members, calls, instances and invocationCallOrder, which are all lists. The items in the calls list are the arguments with which the function was called. The "instances" list stores the value of 'this' for each call to the function. This is useful for retrieving instances from a constructor. The invocationCallOrder lists the order in which the mock was called in relation to all mock calls, starting at 1.
.mockReturnValueOnce(value)Pushes the given value onto a FIFO queue of return values for the function.
.mockReturnValue(value)Sets the default return value for the function.
.mockImplementationOnce(function)Pushes the given mock implementation onto a FIFO queue of mock implementations for the function.
.mockImplementation(function)Sets the default mock implementation for the function.
.mockReturnThis()Syntactic sugar for:
mockFn.mockImplementation(function () {
return this;
});
In case both .mockImplementationOnce() / .mockImplementation() and .mockReturnValueOnce() / .mockReturnValue() are called. The priority of which to use is based on what is the last call:
.mockReturnValueOnce() or .mockReturnValue(), use the specific return value or default return value. If specific return values are used up or no default return value is set, fall back to try .mockImplementation();.mockImplementationOnce() or .mockImplementation(), run the specific implementation and return the result or run default implementation and return the result..withImplementation(function, callback)Temporarily overrides the default mock implementation within the callback, then restores it's previous implementation.
If the callback is async or returns a thenable, withImplementation will return a promise. Awaiting the promise will await the callback and reset the implementation.