UNPKG

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