Testdouble.js
A minimal test double library for TDD with JavaScript
Install / Use
/learn @testdouble/Testdouble.jsREADME
testdouble.js (AKA td.js)
Welcome! Are you writing JavaScript tests and in the market for a mocking library to fake out real things for you? testdouble.js is an opinionated, carefully-designed test double library maintained by, oddly enough, a software agency that's also named Test Double. (The term "test double" was coined by Gerard Meszaros in his book xUnit Test Patterns.)
If you practice test-driven development, testdouble.js was designed to promote terse, clear, and easy-to-understand tests. There's an awful lot to cover, so please take some time and enjoy our documentation, which is designed to show you how to make the most out of test doubles in your tests.
This library was designed to work for both Node.js and browser interpeters. It's also test-framework agnostic, so you can plop it into a codebase using Jasmine, Mocha, Tape, Jest, or our own teenytest.
Install
$ npm install -D testdouble
If you just want to fetch the browser distribution, you can also curl it from unpkg.
We recommend requiring the library in a test helper and setting it globally for
convenience to the shorthand td:
// ES import syntax
import * as td from 'testdouble'
// CommonJS modules (e.g. Node.js)
globalThis.td = require('testdouble')
// Global set in our browser distribution
window.td
(You may need to configure your linter to ignore the td global.
Instructions:
eslint,
standard.)
If you're using testdouble.js in conjunction with another test framework, you may also want to check out one of these extensions:
Getting started
Mocking libraries are more often abused than used effectively, so figuring out how to document a mocking library so as to only encourage healthy uses has proven to be a real challenge. Here are a few paths we've prepared for getting started with testdouble.js:
- The API section of this README so you can get started stubbing and verifying right away
- A 20-minute video overview of the library, its goals, and basic usage
- A comparison between testdouble.js and Sinon.js, in case you've already got experience working with Sinon and you're looking for a high-level overview of how they differ
- The full testdouble.js documentation, which describes at length how to (and how not to) take advantage of the various features of testdouble.js. Its outline is in docs/README.md
Of course, if you're unsure of how to approach writing an isolated test with testdouble.js, we welcome you to open an issue on GitHub to ask a question.
API
td.replace() and td.replaceEsm() for replacing dependencies
The first thing a test double library needs to do is give you a way to replace the production dependencies of your subject under test with fake ones controlled by your test.
We provide a top-level function called td.replace() that operates in two
different modes: CommonJS module replacement and object-property replacement.
Both modes will, by default, perform a deep clone of the real dependency which
replaces all functions it encounters with fake test double functions which can,
in turn, be configured by your test to either stub responses or assert
invocations.
For ES modules, you should use td.replaceEsm(). More details
here.
Module replacement with Node.js
td.replace('../path/to/module'[, customReplacement])
If you're using Node.js and don't mind using the CommonJS require() function
in your tests (you can still use import/export in your production code,
assuming you're compiling it down for consumption by your tests), testdouble.js
uses a library we wrote called quibble
to monkey-patch require() so that your subject will automatically receive your
faked dependencies simply by requiring them. This approach may be familiar if you've used something like
proxyquire, but our focus was to
enable an even more minimal test setup.
Here's an example of using td.replace() in a Node.js test's setup:
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() }
}
In the above example, at the point when src/index is required, the module
cache will be bypassed as index is loaded. If index goes on to subsequently
require any of the td.replace()'d dependencies, it will receive a reference to
the same fake dependencies that were returned to the test.
Because td.replace() first loads the actual file, it will do its best to
return a fake that is shaped just like the real thing. That means that if
loads-purchases exports a function, a test double function will be created and
returned. If generates-invoice exports a constructor, a constructor test
double will be returned, complete with test doubles for all of the original's
static functions and instance methods. If sends-invoice exports a plain
object of function properties, an object will be returned with test double
functions in place of the originals' function properties. In every case, any
non-function properties will be deep-cloned.
There are a few important things to keep in mind about replacing Node.js modules
using td.replace():
- The test must
td.replace()andrequire()everything in a before-each hook, in order to bypass the Node.js module cache and to avoid pollution between tests - Any relative paths passed to
td.replace()are relative from the test to the dependency. This runs counter to how some other tools do it, but we feel it makes more sense - The test suite (usually in a global after-each hook) must call
td.reset()to ensure the realrequire()function and dependency modules are restored after each test case.
Default exports with ES modules
If your modules are written in the ES module syntax and they specify default
exports (e.g. export default function loadsPurchases()), but are actually
transpiled to CommonJS, just remember that you'll need to reference .default
when translating to the CJS module format.
That means instead of this:
loadsPurchases = td.replace('../src/loads-purchases')
You probably want to assign the fake like this:
loadsPurchases = td.replace('../src/loads-purchases').default
Property replacement
td.replace(containingObject, nameOfPropertyToReplace[, customReplacement])
If you're running tests outside Node.js or otherwise injecting dependencies
manually (or with a DI tool like
dependable), then you may still use
td.replace to automatically replace things if they're referenceable as
properties on an object.
To illustrate, suppose our subject depends on app.signup below:
app.signup = {
onSubmit: function () {},
onCancel: function () {}
}
If our goal is to replace app.signup during a test of app.user.create(),
our test setup might look like this:
let signup, subject
module.exports = {
beforeEach: function () {
signup = td.replace(app, 'signup')
subject = app.user
}
// …
afterEach: function () { td.reset() }
}
td.replace() will always return the newly-created fake imitation, even though
in this case it's obviously still referenceable by the test and subject alike
with app.signup. If we had wanted to only replace the onCancel function for
whatever reason (though in this case, that would smell like a partial
mock), we
could have called td.replace(app.signup, 'onCancel'), instead.
Remember to call td.reset() in an after-each hook (preferably globally so one
doesn't have to remember to do so in each and every test) so that testdouble.js
can replace the original. This is crucial to avoiding hard-to-debug test
pollution!
Specifying a custom replacement
The library's imitation feature is pretty sophisticated, but it's not perfect. It's also going to be pretty slow on large, complex objects. If you'd like to specify exactly what to replace a real dependency with, you can do so in either of the above modes by providing a final optional argument.
When replacing a Node.js module:
generatesInvoice = td.replace('../generates-invoice', {
generate: td.func('a generate function'),
name: 'fake invoices'
