testdouble.js

raw JSON →
3.20.2 verified Sat Apr 25 auth: no javascript

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.

error ReferenceError: td is not defined
cause Attempting to use the `td` global without properly importing or requiring `testdouble` and assigning it to `globalThis.td`.
fix
Ensure 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)
cause Your linter (e.g., ESLint) does not recognize `td` as a global variable, particularly if you've opted for the global assignment pattern.
fix
Add 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'.
cause Incorrect usage of `td.replaceEsm()`, passing 'default' as the second argument when it expects a specific export name or a path for the default export.
fix
When replacing the default export of an ESM, you typically provide the module path and then the *keyword* 'default' as the second argument, e.g., td.replaceEsm('./my-module', 'default', () => ({...})). If targeting a named export, provide its specific name.
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.
fix Review the official documentation for `td.replace()` and `td.replaceEsm()`. Ensure `td.replaceEsm()` is used for ES Modules, requiring specific build tool configurations (e.g., Vite, Webpack, Node's `--loader`) to work correctly.
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.
fix Configure your linter (e.g., ESLint, StandardJS) to acknowledge `td` as a global variable. Refer to your linter's documentation for adding global exclusions.
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.
fix Always use `td.replaceEsm()` when mocking ES Modules. Be aware that `td.replaceEsm()` might require specific test environment setups (e.g., Node.js with `--loader` flags or bundler configurations like Vite/Webpack) to intercept module imports effectively. Consult the `testdouble.js` documentation for detailed guidance on your module system.
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.
fix Focus on testing behavior rather than implementation details. Use test doubles only for external dependencies that cannot be controlled in a test. Avoid mocking value objects or closely coupled internal components. Refer to the 'xUnit Test Patterns' book and `testdouble.js`'s extensive documentation on healthy TDD practices.
npm install testdouble
yarn add testdouble
pnpm add testdouble

This quickstart demonstrates how to create a test double for a class dependency using `td.object`, stub its methods with `td.when().thenReturn()`, and verify interactions with `td.verify()` within a `mocha`-like test structure. It also shows `td.reset()` for cleanup and basic argument matching.

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 });
  });
});