# testdouble.js for JavaScript TDD: dependency replacement and module fakes

> testdouble.js is a small, framework-independent test double library for Node.js and browser JavaScript. Its main distinction is opinionated dependency replacement, including CommonJS modules, ES modules, and object properties.

**testdouble/testdouble.js** — A minimal test double library for TDD with JavaScript

- Repository: https://github.com/testdouble/testdouble.js
- Stars: 1,426 · Forks: 141
- Language: JavaScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/testdouble-testdouble-js

## Why testdouble.js targets isolated JavaScript tests

testdouble.js addresses a specific testing problem: replacing production dependencies with controlled fakes so a subject under test can run without its real collaborators. The README positions the library for practitioners of test-driven development who want tests that are terse and easy to understand. Its stated audience includes developers using Jasmine, Mocha, Tape, Jest, or teenytest, although the library itself is described as test-framework agnostic.

The term test double covers more than one kind of fake. In this library, replacement functions can be configured to return stub responses or to assert invocations. The README says that td.replace() can replace both CommonJS modules and object properties. For ES modules, it directs users to td.replaceEsm(). This makes the library useful when a test needs to control a dependency at its source rather than changing the behavior of every method manually.

The project also works in Node.js and browser interpreters. That portability comes with some setup considerations. The package exposes different entry points through its exports map: the CommonJS entry is lib/index.js, the Node.js ES module entry is lib/index.mjs, and browser imports use dist/testdouble.mjs. The browser distribution can also be fetched from unpkg. A team adopting the library should therefore check which module system and browser build its test environment actually loads.

## The replacement mechanism behind td.replace() and td.replaceEsm()

For Node.js CommonJS modules, td.replace() uses quibble to monkey-patch require(). According to the README, this means the subject receives a faked dependency when it requires the module after the test has registered the replacement. The test replaces dependencies before requiring the subject, and the subject can then use those same fake references through its own require() calls.

The replacement is shaped after the real module. By default, td.replace() deep clones the real dependency and replaces functions with fake test double functions. If a module exports a function, the returned value is a function-shaped test double. If it exports a constructor, the replacement includes fakes for its static functions and instance methods. If it exports an object containing functions, those properties are replaced while non-function properties are deep-cloned. This approach allows tests to preserve the dependency's shape without running its original behavior.

The replacement process has a cache-sensitive requirement. The README says that replacements and the subject's require() call should happen in a before-each hook. This bypasses the Node.js module cache and helps prevent pollution between tests. After each test, td.reset() is used in the documented setup. A test that requires the subject before registering replacements, or that fails to reset state, may therefore observe different behavior from the documented sequence.

For ES modules, the README directs users to td.replaceEsm() and links to separate documentation for that flow. The repository contains examples for Node.js ESM, an ESM auto-loader, and pnpm-based ESM, but the README does not state which exact Node.js versions or loader configurations are required for every ES module example.

## Installing testdouble.js and setting the td shorthand

Install the package as a development dependency from the project root. The README gives this command:

```bash
npm install -D testdouble
```

The installation is intended for test code rather than production dependencies. After installation, the README recommends loading the library from a test helper and assigning it globally for the short name td. An ES module test can import the namespace directly:

```js
import * as td from 'testdouble'
```

A CommonJS test can assign the required package to globalThis:

```js
globalThis.td = require('testdouble')
```

The browser distribution exposes a global named window.td. The README warns that a linter may need to ignore the td global. The package metadata confirms that consumers can import the package through the main entry, the browser ESM entry, or the explicit dist/testdouble.mjs export, depending on the environment.

## A first CommonJS replacement test

The following is the repository's documented setup shape. It registers replacements for three modules in beforeEach, requires the subject after those registrations, and resets testdouble state in afterEach.

```js
let loadsPurchases, generatesInvoice, sendsInvoice, subject
module.exports = {
  beforeEach: () => {
    loadsPurchases = td.replace('../src/loads-purchases')
    generatesInvoice = td.replace('../src/generates-invoice')
    sendsInvoice = td.replace('../src/sends-invoice')
    subject = require('../src/index')
  },
  afterEach: function () { td.reset() }
}
```

The important detail is ordering. The test calls td.replace() for each dependency before requiring src/index. The README states that src/index is loaded with the module cache bypassed, so its later require() calls receive the fake dependencies returned to the test. The variables then hold those references, which can be configured for stub responses or used for invocation assertions. The example does not show a complete assertion or stubbing example, so it does not establish the exact call syntax for those operations.

The relative paths in the example are repository-local placeholders. They must point to the actual modules used by the subject under test. The README also warns that path handling has important conditions, but the excerpt ends before listing them. Module-path resolution therefore needs verification in the project's own test configuration rather than an assumption based on installation alone.

## Framework independence and extension packages

The core package does not require a particular test framework. The README lists Jasmine, Mocha, Tape, Jest, and teenytest as examples, and the repository includes examples for Jest, AVA, Web Test Runner, Webpack, and browser-oriented workflows. This is useful when a codebase already has a framework and only needs a dependency replacement tool.

The README separately links to testdouble-jest, testdouble-chai, testdouble-jasmine, testdouble-qunit, and testdouble-vitest. Those are integration extensions rather than the core package's documented API. Their presence indicates that framework-specific behavior can be addressed outside the main library, but the README does not describe the features or installation steps of each extension. A team should not assume that installing testdouble alone activates every framework adapter.

The package also includes TypeScript declaration files, index.d.ts, and a notypescript directory in the repository. The package metadata maps the root types entry to index.d.ts. This is useful for projects that consume JavaScript libraries from TypeScript, although the README does not provide a TypeScript setup tutorial.

## Where testdouble.js is the wrong choice

The clearest limitation is the cost of following its module-loading rules. CommonJS replacement depends on registering td.replace() calls before loading the subject, bypassing the module cache for that test, and calling td.reset() afterward. This is manageable for an isolated unit test, but it is more stateful than manually passing a small fake into a function. Tests that load modules at unpredictable times or keep shared module state across cases need particular care.

The ES module path is less explicit in the README. It names td.replaceEsm(), but directs readers to separate documentation for the exact replacement mechanism. The repository's ESM examples show that ESM is considered, yet the README does not promise compatibility with every loader or package configuration. Projects using unusual runtime loaders should inspect those examples before selecting the library.

A hand-written fake is a reasonable alternative when a dependency has only one or two stable interfaces. The test can pass an ordinary object with controlled functions, avoiding require() patching and module-cache sequencing. testdouble.js offers a different approach: it replaces the dependency where the subject obtains it and creates fakes shaped from the real export. Choose the smaller approach when the fake is trivial and the library when preserving a dependency's shape or replacing several modules is more useful.

## Conclusion

Adopt testdouble.js when JavaScript TDD needs concise, framework-independent dependency replacement, especially for CommonJS or supported ES module flows. Do not adopt it merely because a project uses mocking, or without checking module loading, cache cleanup, and framework integration first. The README does not document rollback behavior or GitHub releases, and the repository has no GitHub releases, so version changes should be checked against the package metadata and changelog before adoption.

## FAQ

### How do I install testdouble.js for JavaScript tests?

Install it as a development dependency with `npm install -D testdouble`. The README recommends loading it from a test helper, either through an ES import or by assigning `require('testdouble')` to `globalThis.td`.

### How does testdouble.js replace a CommonJS dependency?

The README says `td.replace()` uses quibble to monkey-patch Node.js `require()`. Register replacements before requiring the subject, then call `td.reset()` after the test.

### Does testdouble.js require a specific test framework?

No. The README describes testdouble.js as test-framework agnostic and lists Jasmine, Mocha, Tape, Jest, and teenytest as examples. Separate extension packages are available for some frameworks.

### Does testdouble.js support ES modules?

The README directs ES module users to `td.replaceEsm()`. It does not document all runtime or loader requirements, so the project's ESM documentation and examples should be checked for the chosen setup.

### Is testdouble.js actively maintained?

The repository is not archived, and the last push was on 2026-10-12. The repository has no GitHub releases, so release history is not available from the repository metadata.

## Sources

- [Issues](https://github.com/testdouble/testdouble.js/issues)
- [License: MIT](https://github.com/testdouble/testdouble.js/blob/main/LICENSE)
- [README](https://github.com/testdouble/testdouble.js/blob/main/README.md)
- [testdouble/testdouble.js on GitHub](https://github.com/testdouble/testdouble.js)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/testdouble-testdouble-js
