UNPKG

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