UNPKG

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