testdouble.js
raw JSON →testdouble.js (AKA td.js) is an opinionated, minimalist test double library for JavaScript and TypeScript, designed to facilitate test-driven development (TDD). It provides robust mechanisms for creating mocks, stubs, and spies to replace real dependencies within tests, promoting terse, clear, and easy-to-understand test suites. As of version 3.20.2, the library is actively maintained by Test Double, a software agency. It maintains a steady release cadence for bug fixes and minor features within its major versions. The library is framework-agnostic, compatible with popular test runners like Jest, Mocha, and Jasmine, and functions reliably in both Node.js and browser environments. Its primary differentiator is its strong opinions on TDD practices, aiming to guide developers toward healthier mocking patterns rather than simply offering a comprehensive feature set without guidance.
Common errors
error ReferenceError: td is not defined ↓
import * as td from 'testdouble' (ESM) or globalThis.td = require('testdouble') (CommonJS) is executed before any td calls in your test files or setup scripts. error ESLint: 'td' is not defined. (no-undef) ↓
td to your linter's global configuration. For ESLint, you might add 'td': true under the globals section in your .eslintrc file. error Error: td.replaceEsm() requires two string arguments for the module and export name, not 'default'. ↓
td.replaceEsm('./my-module', 'default', () => ({...})). If targeting a named export, provide its specific name. Warnings
breaking testdouble.js v3 introduced significant changes to how module replacement works, particularly with `td.replaceEsm()` to correctly handle ES Modules. Previous versions might have relied on less robust or different patterns for module replacement which are no longer supported or behave differently. ↓
gotcha The library recommends optionally setting `td` globally (e.g., `globalThis.td = require('testdouble')`). This can lead to linting errors (e.g., 'td is not defined') if your linter is not configured to recognize `td` as a global variable. ↓
gotcha Understanding the distinction between `td.replace()` and `td.replaceEsm()` is critical for correct module mocking. `td.replaceEsm()` is specifically designed for ES Modules and has different usage requirements and implications compared to `td.replace()` which primarily targets CommonJS or default exports. ↓
gotcha Like many mocking libraries, `testdouble.js` can be abused, leading to over-mocked, brittle tests that provide little confidence. The library's maintainers themselves caution against common anti-patterns. ↓
Install
npm install testdouble yarn add testdouble pnpm add testdouble Imports
- td wrong
const td = require('testdouble')correctimport * as td from 'testdouble' - td wrong
import * as td from 'testdouble'; globalThis.td = td;correctglobalThis.td = require('testdouble') - TestDouble
import type * as TestDouble from 'testdouble' - td.replaceEsm wrong
import { replaceEsm } from 'testdouble'correctimport * as td from 'testdouble'; td.replaceEsm('./my-module', 'default', () => ({ ... }))
Quickstart
import * as td from 'testdouble';
import assert from 'assert';
// Simulate a dependency module
class EmailService {
send(to: string, subject: string, body: string): boolean {
console.log(`Sending email to ${to}: ${subject}`);
return true;
}
}
// Simulate a module under test that uses EmailService
class UserService {
constructor(private emailService: EmailService) {}
registerUser(email: string, username: string): boolean {
if (!this.emailService.send(email, 'Welcome!', `Hello ${username}, welcome!`)) {
return false;
}
return true;
}
}
// Test setup
describe('UserService with testdouble', () => {
let mockEmailService: EmailService;
let userService: UserService;
beforeEach(() => {
// Replace the EmailService class with a test double
mockEmailService = td.object<EmailService>(['send']);
userService = new UserService(mockEmailService);
});
afterEach(() => {
td.reset(); // Clean up all test doubles after each test
});
it('should register a user and send a welcome email', () => {
// Stub the 'send' method to always return true
td.when(mockEmailService.send('user@example.com', 'Welcome!', td.matchers.isA(String)))
.thenReturn(true);
const result = userService.registerUser('user@example.com', 'testuser');
assert.strictEqual(result, true);
// Verify that the send method was called with expected arguments
td.verify(mockEmailService.send('user@example.com', 'Welcome!', 'Hello testuser, welcome!'), { times: 1 });
});
it('should handle email sending failure', () => {
// Stub the 'send' method to return false
td.when(mockEmailService.send(td.matchers.anything(), td.matchers.anything(), td.matchers.anything()))
.thenReturn(false);
const result = userService.registerUser('fail@example.com', 'failuser');
assert.strictEqual(result, false);
td.verify(mockEmailService.send(td.matchers.anything(), td.matchers.anything(), td.matchers.anything()), { times: 1 });
});
});