UNPKG

25.3 kBMarkdownView Raw
1# testdouble.js (AKA td.js)
2
3[![npmjs](https://img.shields.io/badge/npm-testdouble-red.svg)](https://www.npmjs.com/package/testdouble)
4[![unpkg](https://img.shields.io/badge/unpkg-download-blue.svg)](https://unpkg.com/testdouble/dist/)
5
6Welcome! Are you writing JavaScript tests and in the market for a mocking
7library to fake out real things for you? testdouble.js is an opinionated,
8carefully-designed test double library maintained by, oddly enough, a software
9agency that's also named [Test Double](http://testdouble.com). (The term "test
10double" was coined by Gerard Meszaros in his book [xUnit Test
11Patterns](http://xunitpatterns.com/Test%20Double.html).)
12
13If you practice test-driven development, testdouble.js was designed to promote
14terse, clear, and easy-to-understand tests. There's an awful lot to cover, so
15please take some time and enjoy our documentation, which is designed to show you
16how to make the most out of test doubles in your tests.
17
18This library was designed to work for both Node.js and browser interpeters. It's
19also test-framework agnostic, so you can plop it into a codebase using Jasmine,
20Mocha, Tape, Jest, or our own
21[teenytest](https://github.com/testdouble/teenytest).
22
23## Install
24
25```
26$ npm install -D testdouble
27```
28
29If you just want to fetch the browser distribution, you can also curl it from
30[unpkg](https://unpkg.com/testdouble/dist/).
31
32We recommend requiring the library in a test helper and setting it globally for
33convenience to the shorthand `td`:
34
35```js
36// ES import syntax
37import * as td from 'testdouble'
38
39// CommonJS modules (e.g. Node.js)
40globalThis.td = require('testdouble')
41
42// Global set in our browser distribution
43window.td
44```
45
46(You may need to configure your linter to ignore the `td` global.
47Instructions:
48[eslint](https://eslint.org/docs/user-guide/configuring#specifying-globals),
49[standard](https://github.com/standard/standard/#i-use-a-library-that-pollutes-the-global-namespace-how-do-i-prevent-variable-is-not-defined-errors).)
50
51If you're using testdouble.js in conjunction with another test framework, you
52may also want to check out one of these extensions:
53
54* [testdouble-jest](https://github.com/testdouble/testdouble-jest)
55* [testdouble-chai](https://github.com/basecase/testdouble-chai)
56* [testdouble-jasmine](https://github.com/BrianGenisio/testdouble-jasmine)
57* [testdouble-qunit](https://github.com/alexlafroscia/testdouble-qunit/tree/master/packages/testdouble-qunit)
58* [testdouble-vitest](https://github.com/mcous/testdouble-vitest)
59
60## Getting started
61
62Mocking libraries are more often abused than used effectively, so figuring out
63how to document a mocking library so as to only encourage healthy uses has
64proven to be a real challenge. Here are a few paths we've prepared for getting
65started with testdouble.js:
66
67* The [API section of this README](#api) so you can get started stubbing and
68 verifying right away
69* A [20-minute
70 video](http://blog.testdouble.com/posts/2016-06-05-happier-tdd-with-testdouble-js)
71 overview of the library, its goals, and basic usage
72* A [comparison between testdouble.js and
73 Sinon.js](http://blog.testdouble.com/posts/2016-03-13-testdouble-vs-sinon.html),
74 in case you've already got experience working with Sinon and you're looking
75 for a high-level overview of how they differ
76* The full testdouble.js [documentation](/docs), which describes at length how
77 to (and how not to) take advantage of the various features of testdouble.js.
78 Its outline is in [docs/README.md](/docs#readme)
79
80Of course, if you're unsure of how to approach writing an isolated test with
81testdouble.js, we welcome you to [open an issue on GitHub to ask a
82question](https://github.com/testdouble/testdouble.js/issues/new).
83
84## API
85
86### `td.replace()` and `td.replaceEsm()` for replacing dependencies
87
88The first thing a test double library needs to do is give you a way to replace
89the production dependencies of your [subject under
90test](https://github.com/testdouble/contributing-tests/wiki/Subject) with fake
91ones controlled by your test.
92
93We provide a top-level function called `td.replace()` that operates in two
94different modes: CommonJS module replacement and object-property replacement.
95Both modes will, by default, perform a deep clone of the real dependency which
96replaces all functions it encounters with fake test double functions which can,
97in turn, be configured by your test to either stub responses or assert
98invocations.
99
100For ES modules, you should use `td.replaceEsm()`. More details
101[here](docs/7-replacing-dependencies.md#how-module-replacement-works-for-es-modules-using-import).
102
103#### Module replacement with Node.js
104
105**`td.replace('../path/to/module'[, customReplacement])`**
106
107If you're using Node.js and don't mind using the CommonJS `require()` function
108in your tests (you can still use `import`/`export` in your production code,
109assuming you're compiling it down for consumption by your tests), testdouble.js
110uses a library we wrote called [quibble](https://github.com/testdouble/quibble)
111to monkey-patch `require()` so that your subject will automatically receive your
112faked dependencies simply by requiring them. This approach may be familiar if you've used something like
113[proxyquire](https://github.com/thlorenz/proxyquire), but our focus was to
114enable an even more minimal test setup.
115
116Here's an example of using `td.replace()` in a Node.js test's setup:
117
118```js
119let loadsPurchases, generatesInvoice, sendsInvoice, subject
120module.exports = {
121 beforeEach: () => {
122 loadsPurchases = td.replace('../src/loads-purchases')
123 generatesInvoice = td.replace('../src/generates-invoice')
124 sendsInvoice = td.replace('../src/sends-invoice')
125 subject = require('../src/index')
126 }
127 //…
128 afterEach: function () { td.reset() }
129}
130```
131
132In the above example, at the point when `src/index` is required, the module
133cache will be bypassed as `index` is loaded. If `index` goes on to subsequently
134require any of the `td.replace()`'d dependencies, it will receive a reference to
135the same fake dependencies that were returned to the test.
136
137Because `td.replace()` first loads the actual file, it will do its best to
138return a fake that is shaped just like the real thing. That means that if
139`loads-purchases` exports a function, a test double function will be created and
140returned. If `generates-invoice` exports a constructor, a constructor test
141double will be returned, complete with test doubles for all of the original's
142static functions and instance methods. If `sends-invoice` exports a plain
143object of function properties, an object will be returned with test double
144functions in place of the originals' function properties. In every case, any
145non-function properties will be deep-cloned.
146
147There are a few important things to keep in mind about replacing Node.js modules
148using `td.replace()`:
149
150* The test must `td.replace()` and `require()` everything in a before-each hook,
151 in order to bypass the Node.js module cache and to avoid pollution between
152 tests
153* Any relative paths passed to `td.replace()` are relative *from the test to the
154 dependency*. This runs counter to how some other tools do it, but we feel it
155 makes more sense
156* The test suite (usually in a global after-each hook) must call `td.reset()` to
157 ensure the real `require()` function and dependency modules are restored after
158 each test case.
159
160##### Default exports with ES modules
161
162If your modules are written in the ES module syntax and they specify default
163exports (e.g. `export default function loadsPurchases()`), but are actually
164transpiled to CommonJS, just remember that you'll need to reference `.default`
165when translating to the CJS module format.
166
167That means instead of this:
168
169```js
170loadsPurchases = td.replace('../src/loads-purchases')
171```
172
173You probably want to assign the fake like this:
174
175```js
176loadsPurchases = td.replace('../src/loads-purchases').default
177```
178
179#### Property replacement
180
181**`td.replace(containingObject, nameOfPropertyToReplace[, customReplacement])`**
182
183If you're running tests outside Node.js or otherwise injecting dependencies
184manually (or with a DI tool like
185[dependable](https://github.com/testdouble/dependable)), then you may still use
186`td.replace` to automatically replace things if they're referenceable as
187properties on an object.
188
189To illustrate, suppose our subject depends on `app.signup` below:
190
191``` js
192app.signup = {
193 onSubmit: function () {},
194 onCancel: function () {}
195}
196```
197
198If our goal is to replace `app.signup` during a test of `app.user.create()`,
199our test setup might look like this:
200
201```js
202let signup, subject
203module.exports = {
204 beforeEach: function () {
205 signup = td.replace(app, 'signup')
206 subject = app.user
207 }
208 // …
209 afterEach: function () { td.reset() }
210}
211```
212
213`td.replace()` will always return the newly-created fake imitation, even though
214in this case it's obviously still referenceable by the test and subject alike
215with `app.signup`. If we had wanted to only replace the `onCancel` function for
216whatever reason (though in this case, that would smell like a [partial
217mock](https://github.com/testdouble/contributing-tests/wiki/Partial-Mock)), we
218could have called `td.replace(app.signup, 'onCancel')`, instead.
219
220Remember to call `td.reset()` in an after-each hook (preferably globally so one
221doesn't have to remember to do so in each and every test) so that testdouble.js
222can replace the original. This is crucial to avoiding hard-to-debug test
223pollution!
224
225#### Specifying a custom replacement
226
227The library's [imitation
228feature](https://github.com/testdouble/testdouble.js/blob/main/src/imitate/index.js)
229is pretty sophisticated, but it's not perfect. It's also going to be pretty slow
230on large, complex objects. If you'd like to specify exactly what to replace a
231real dependency with, you can do so in either of the above modes by providing a
232final optional argument.
233
234When replacing a Node.js module:
235
236```js
237generatesInvoice = td.replace('../generates-invoice', {
238 generate: td.func('a generate function'),
239 name: 'fake invoices'
240})
241```
242
243When replacing a property:
244
245```js
246signup = td.replace(app, 'signup', {
247 onSubmit: td.func('fake submit handler'),
248 onCancel: function () { throw Error('do not call me') }
249})
250```
251
252### `td.func()`, `td.object()`, `td.constructor()`, `td.instance()` and `td.imitate()` to create test doubles
253
254`td.replace()`'s imitation and injection convenience is great when your
255project's build configuration allows for it, but in many cases you'll want or
256need the control to create fake things directly. Each creation function can
257either imitate a real thing or be specified by passing a bit of configuration.
258
259Each test double creation function is very flexible and can take a variety of
260inputs. What gets returned generally depends on the number and type of configuration
261parameters passed in, so we'll highlight each supported usage separately with an
262example invocation:
263
264#### `td.func()`
265
266The `td.func()` function (also available as `td.function()`) returns a test
267double function and can be called in three modes:
268
269* **`td.func(someRealFunction)`** - returns a test double function of the same
270 `name`, including a deep
271 [imitation](https://github.com/testdouble/testdouble.js/blob/main/src/imitate/index.js)
272 of all of its custom properties
273* **`td.func()`** - returns an anonymous test double function that can be used
274 for stubbing and verifying any calls against it, but whose error messages and
275 debugging output won't have a name to trace back to it
276* **`td.func('some name')`** - returns a test double function named `'some
277 name'`, which will appear in any error messages as well as the debug info
278 returned by passing the returned test double into
279 [td.explain()](/docs/9-debugging.md#tdexplainsometestdouble)
280* **`td.func<Type>()`** - returns a test double function imitating the passed type.
281 Examples and more details can be found in [using with TypeScript](/docs/10-using-with-typescript.md)
282
283#### `td.object()`
284
285The `td.object()` function returns an object containing test double functions,
286and supports three types of invocations:
287
288* **`td.object(realObject)`** - returns a deep
289 [imitation](https://github.com/testdouble/testdouble.js/blob/main/src/imitate/index.js)
290 of the passed object, where each function is replaced with a test double function
291 named for the property path (e.g. If `realObject.invoices.send()` was a
292 function, the returned object would have property `invoices.send` set to a
293 test double named `'.invoices.send'`)
294* **`td.object(['add', 'subtract'])`** - returns a plain JavaScript object
295 containing two properties `add` and `subtract` that are both assigned to test
296 double functions named `'.add'` and `'.subtract'`, respectively
297* **`td.object('a Person'[, {excludeMethods: ['then']})`** - when passed with no
298 args or with a string name as the first argument, returns an [ES
299 Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).
300 The proxy will automatically intercept any call made to it and shunt in a test
301 double that can be used for stubbing or verification. More details can be
302 found in [our full docs](/docs/4-creating-test-doubles.md#objectobjectname)
303* **`td.object<Interface>()`** - returns an object with methods exposed as test doubles
304 that are typed according to the passed interface. Examples and more details can be found in
305 [using with TypeScript](/docs/10-using-with-typescript.md)
306
307#### `td.constructor()`
308
309If your code depends on ES classes or functions intended to be called with
310`new`, then the `td.constructor()` function can replace those dependencies as
311well.
312
313* **`td.constructor(RealConstructor)`** - returns a constructor whose calls can
314 be verified and whose static and `prototype` functions have all been replaced
315 with test double functions using the same
316 [imitation](https://github.com/testdouble/testdouble.js/blob/main/src/imitate/index.js)
317 mechanism as `td.func(realFunction)` and `td.object(realObject)`
318* **`td.constructor(['select', 'save'])`** - returns a constructor with `select`
319 and `save` properties on its `prototype` object set to test double functions
320 named `'#select'` and `'#save'`, respectively
321
322When replacing a constructor, typically the test will configure stubbing &
323verification by directly addressing its prototype functions. To illustrate, that
324means in your test you might write:
325
326```js
327const FakeConstructor = td.constructor(RealConstructor)
328td.when(FakeConstructor.prototype.doStuff()).thenReturn('ok')
329
330subject(FakeConstructor)
331```
332
333So that in your production code you can:
334
335```js
336const subject = function (SomeConstructor) {
337 const thing = new SomeConstructor()
338 return thing.doStuff() // returns "ok"
339}
340```
341
342#### `td.instance()`
343
344As a shorthand convenience, `td.instance()` function will call
345`td.constructor()` and return a `new` instance of the fake constructor function
346it returns.
347
348The following code snippets are functionally equivalent:
349
350```js
351const fakeObject = td.instance(RealConstructor)
352```
353
354```js
355const FakeConstructor = td.constructor(RealConstructor)
356const fakeObject = new FakeConstructor()
357```
358
359#### `td.imitate()`
360
361**`td.imitate(realThing[, name])`**
362
363If you know you want to imitate something, but don't know (or care) whether it's
364a function, object, or constructor, you can also just pass it to `td.imitate()`
365with an optional name parameter.
366
367### `td.when()` for stubbing responses
368
369**`td.when(__rehearsal__[, options])`**
370
371Once you have your subject's dependencies replaced with test double functions,
372you'll want to be able to stub return values (and other sorts of responses)
373when the subject invokes the test double in the way that the test expects.
374
375To make stubbing configuration easy to read and grep, `td.when()`'s first
376argument isn't an argument at all, but rather a placeholder to demonstrate the
377way you're expecting the test double to be invoked by the subject, like so:
378
379```js
380const increment = td.func()
381td.when(increment(5)).thenReturn(6)
382```
383
384We would say that `increment(5)` is "rehearsing the invocation". Note that by
385default, a stubbing is only satisfied when the subject calls the test double
386exactly as it was rehearsed. This can be customized with [argument
387matchers](/docs/5-stubbing-results.md#loosening-stubbings-with-argument-matchers),
388which allow for rehearsals that do things like
389`increment(td.matchers.isA(Number))` or `save(td.matchers.contains({age: 21}))`.
390
391Also note that, `td.when()` takes an [optional configuration
392object](/docs/5-stubbing-results.md#configuring-stubbings) as a second
393parameter, which enables advanced usage like ignoring extraneous arguments and
394limiting the number of times a stubbing can be satisfied.
395
396Calling `td.when()` returns a number of functions that allow you to specify your
397desired outcome when the test double is invoked as demonstrated by your
398rehearsal. We'll begin with the most common of these: `thenReturn`.
399
400#### `td.when().thenReturn()`
401
402**`td.when(__rehearsal__[, options]).thenReturn('some value'[, more, values])`**
403
404The simplest example is when you want to return a specific value in exchange for
405a known argument, like so:
406
407```js
408const loadsPurchases = td.replace('../src/loads-purchases')
409td.when(loadsPurchases(2018, 8)).thenReturn(['a purchase', 'another'])
410```
411
412Then, in the hands of your subject under test:
413
414```js
415loadsPurchases(2018, 8) // returns `['a purchase', 'another']`
416loadsPurchases(2018, 7) // returns undefined, since no stubbing was satisfied
417```
418
419If you're not used to stubbing, it may seem contrived to think a test will know
420exactly what argument to pass in and expect back from a dependency, but in an
421isolated unit test this is not only feasible but entirely normal and expected!
422Doing so helps the author ensure the test remains minimal and obvious to
423future readers.
424
425Note as well that subsequent matching invocations can be stubbed by passing
426additional arguments to `thenReturn()`, like this:
427
428```js
429const hitCounter = td.func()
430td.when(hitCounter()).thenReturn(1, 2, 3, 4)
431
432hitCounter() // 1
433hitCounter() // 2
434hitCounter() // 3
435hitCounter() // 4
436hitCounter() // 4
437```
438
439#### `td.when().thenResolve()` and `td.when().thenReject()`
440
441**`td.when(__rehearsal__[, options]).thenResolve('some value'[, more, values])`**
442
443**`td.when(__rehearsal__[, options]).thenReject('some value'[, more, values])`**
444
445The `thenResolve()` and `thenReject()` stubbings will take whatever value is
446passed to them and wrap it in an immediately resolved or rejected promise,
447respectively. By default testdouble.js will use whatever `Promise` is globally
448defined, but you can specify your own like this:
449
450```js
451td.config({promiseConstructor: require('bluebird')})`
452```
453
454Because the Promise spec indicates that all promises must tick the event loop,
455keep in mind that any stubbing configured with `thenResolve` or `thenReject`
456must be managed as an asynchronous test (consult your test framework's
457documentation if you're not sure).
458
459#### `td.when().thenCallback()`
460
461**`td.when(__rehearsal__[, options]).thenCallback('some value'[,other,
462args])`**
463
464The `thenCallback()` stubbing will assume that the rehearsed invocation has an
465additional final argument that takes a callback function. When this stubbing is
466satisfied, testdouble.js will invoke that callback function and pass in whatever
467arguments were sent to `thenCallback()`.
468
469To illustrate, consider this stubbing:
470
471```js
472const readFile = td.replace('../src/read-file')
473td.when(readFile('my-secret-doc.txt')).thenCallback(null, 'secrets!')
474```
475
476Then, the subject might invoke readFile and pass an anonymous function:
477
478```js
479readFile('my-secret-doc.txt', function (err, contents) {
480 console.log(contents) // will print 'secrets!'
481})
482```
483
484If the callback isn't in the final position, or if the test double also needs to
485return something, callbacks can be configured using the
486[td.callback](/docs/5-stubbing-results.md#callback-apis-with-a-callback-argument-at-an-arbitrary-position)
487argument matcher.
488
489On one hand, `thenCallback()` can be a great way to write fast and clear
490synchronous isolated unit tests of production code that's actually asynchronous.
491On the other hand, if it's necessary to verify the subject behaves correctly
492over multiple ticks of the event loop, you can control this with the [`defer`
493and `delay` options](/docs/5-stubbing-results.md#defer).
494
495#### `td.when().thenThrow()`
496
497**`td.when(__rehearsal__[, options]).thenThrow(new Error('boom'))`**
498
499The `thenThrow()` function does exactly what it says on the tin. Once this
500stubbing is configured, any matching invocations will throw the specified error.
501
502Note that because rehearsal calls invoke the test double function, it's possible
503to configure a `thenThrow` stubbing and then accidentally trigger it when you
504attempt to configure subsequent stubbings or verifications. In these cases,
505you'll need to work around it by re-ordering your configurations or `catch`'ing
506the error.
507
508#### `td.when().thenDo()`
509
510**`td.when(__rehearsal__[, options]).thenDo(function (arg1, arg2) {})`**
511
512For everything else, there is `thenDo()`. `thenDo` takes a function which will
513be invoked whenever satisfied with all the arguments and bound to the same
514`this` context that the test double function was actually invoked with. Whatever
515your `thenDo` function returns will be returned by the test double when the
516stubbing is satisfied. This configuration is useful for covering tricky cases
517not handled elsewhere, and may be a potential extension point for building on
518top of the library's stubbing capabilities.
519
520### `td.verify()` for verifying interactions
521
522**`td.verify(__demonstration__[, options])`**
523
524If you've learned how to stub responses with `td.when()` then you already know
525how to verify an invocation took place with `td.verify()`! We've gone out of our
526way to make the two as symmetrical as possible. You'll find that they have
527matching function signatures, support the same argument matchers, and take the
528same options.
529
530The difference, then, is their purpose. While stubbings are meant to facilitate
531some behavior we want to exercise in our subject, verifications are meant to
532ensure a dependency was called in a particular expected way. Since `td.verify()`
533is an assertion step, it goes [at the
534end](https://github.com/testdouble/contributing-tests/wiki/Arrange-Act-Assert)
535of our test after we've invoked the subject under test.
536
537A trivial example might be:
538
539```js
540module.exports = function shouldSaveThings () {
541 const save = td.replace('../src/save')
542 const subject = require('../src/index')
543
544 subject({name: 'dataz', data: '010101'})
545
546 td.verify(save('dataz', '010101'))
547}
548```
549
550The above will verify that `save` was called with the two specified arguments.
551If the verification fails (say it passed `'010100'` instead), testdouble.js will
552throw a nice long error message to explain how the test double function was
553actually called, hopefully helping you spot the error.
554
555Just like with `td.when()`, more complex cases can be covered with [argument
556matchers](/docs/6-verifying-invocations.md#relaxing-verifications-with-argument-matchers)
557and [configuration
558options](/docs/6-verifying-invocations.md#configuring-verifications).
559
560A word of caution: `td.verify()` should be needed only sparingly. When you
561verify a function was called (as opposed to relying on what it returns) you're
562asserting that your subject has a side effect. Code with lots of side effects is
563bad, so mocking libraries are often abused to make side-effect heavy code easier
564to proliferate. In these cases, refactoring each dependency to return values
565instead is almost always the better design approach. A separate test smell with
566verifying calls is that sometimes—perhaps in the interest of maximal
567completeness—a test will verify an invocation that already satisfied a stubbing,
568but this is almost [provably
569unnecessary](/docs/B-frequently-asked-questions.md#why-shouldnt-i-call-both-tdwhen-and-tdverify-for-a-single-interaction-with-a-test-double).
570
571### `td.listReplacedModules()` for listing the modules that were replaced
572
573**`td.listReplacedModules()`**
574
575
576Use `td.listReplacedModules()` to list the modules that are replaced. This function will return an array of the modules that are
577currently being replaced via `td.replace()` or `td.replaceEsm()`.
578
579The list is in no particular order, and returns the full path to the module that was replaced.
580The path is returned as a `file:` URL as is customary in ESM (this is true even if the
581replaced module was CJS).
582
583For example, if you do this:
584
585```js
586td.replace('../src/save')
587```
588
589Then
590
591```js
592td.listReplacedModules()
593```
594
595will return something like:
596
597```js
598['file:///users/example/code/foo/src/save.js']
599```
600
601### Other functions
602
603For other top-level features in the testdouble.js API, consult the [docs](/docs)
604directory:
605
606* [td.explain()](/docs/9-debugging.md#tdexplainsometestdouble) - for help
607 debugging and introspecting test doubles
608* [td.config()](/docs/C-configuration.md#tdconfig) - for changing globally
609 configurable options
610* [td.reset()](/docs/1-installation.md#resetting-state-between-test-runs) - for
611 resetting testdouble.js state between tests
612* [td.matchers](/docs/5-stubbing-results.md#loosening-stubbings-with-argument-matchers)
613 and [custom matchers](/docs/8-custom-matchers.md#custom-argument-matchers) for
614 configuring more advanced stubbings and verifications