UNPKG

11.4 kBMarkdownView Raw
1fancy-test
2===========
3
4extendable utilities for testing
5
6[![Version](https://img.shields.io/npm/v/fancy-test.svg)](https://npmjs.org/package/fancy-test)
7[![CircleCI](https://circleci.com/gh/jdxcode/fancy-test/tree/master.svg?style=svg)](https://circleci.com/gh/jdxcode/fancy-test/tree/master)
8[![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/jdxcode/fancy-test?branch=master&svg=true)](https://ci.appveyor.com/project/heroku/fancy-test/branch/master)
9[![Greenkeeper](https://badges.greenkeeper.io/jdxcode/fancy-test.svg)](https://greenkeeper.io/)
10[![Known Vulnerabilities](https://snyk.io/test/npm/fancy-test/badge.svg)](https://snyk.io/test/npm/fancy-test)
11[![Downloads/week](https://img.shields.io/npm/dw/fancy-test.svg)](https://npmjs.org/package/fancy-test)
12[![License](https://img.shields.io/npm/l/fancy-test.svg)](https://github.com/jdxcode/fancy-test/blob/master/package.json)
13
14<!-- toc -->
15
16- [Why](#why)
17- [Usage](#usage)
18 * [Stub](#stub)
19 * [Catch](#catch)
20 * [Finally](#finally)
21 * [Nock](#nock)
22 * [Environment Variables](#environment-variables)
23 * [Do](#do)
24 * [Add](#add)
25 * [Stdin Mocking](#stdin-mocking)
26 * [Stdout/Stderr Mocking](#stdoutstderr-mocking)
27 * [Done](#done)
28 * [Retries](#retries)
29 * [Timeout](#timeout)
30 * [Chai](#chai)
31- [Chaining](#chaining)
32- [Custom Plugins](#custom-plugins)
33- [TypeScript](#typescript)
34
35<!-- tocstop -->
36
37Why
38===
39
40Mocha out of the box often requires a lot of setup and teardown code in `beforeEach/afterEach` filters. Using this library, you can get rid of those entirely and build your tests declaratively by chaining functionality together. Using the builtin plugins and your own, you create bits of functionality and chain them together with a concise syntax. It will greatly reduce the amount of repetition in your codebase.
41
42It should be compatible with other testing libraries as well (e.g. jest), but may require a couple small changes. If you're interested, try it out and let me know if it works.
43
44As an example, here is what a test file might look like for an application setup with fancy-test. This chain could partially be stored to a variable for reuse.
45
46```js
47describe('api', () => {
48 fancy
49 // [custom plugin] initializes the db
50 .initDB({withUser: mockDBUser})
51
52 // [custom plugin] uses nock to mock out github API
53 .mockGithubAPI({user: mockGithubUser})
54
55 // [custom plugin] that calls the API of the app
56 .call('POST', '/api/user/foo', {id: mockDBUser.id})
57
58 // add adds to the context object
59 // fetch the newly created data from the API (can return a promise)
60 .add('user', ctx => ctx.db.fetchUserAsync(mockDBUser.id))
61
62 // do just runs arbitary code
63 // check to ensure the operation was successful
64 .do(ctx => expect(ctx.user.foo).to.equal('bar'))
65
66 // it is essentially mocha's it(expectation, callback)
67 // start the test and provide a description
68 .it('POST /api/user/foo updates the user')
69})
70```
71
72Usage
73=====
74
75Setup is pretty easy, just install mocha and fancy-test, then you can use any of the examples below.
76
77Assume the following is before all the examples:
78
79```js
80import {fancy} from 'fancy-test'
81import {expect} from 'chai'
82```
83
84Stub
85----
86
87Stub any object. Like all fancy plugins, it ensures that it is reset to normal after the test runs.
88```js
89import * as os from 'os'
90
91describe('stub tests', () => {
92 fancy
93 .stub(os, 'platform', () => 'foobar')
94 .it('sets os', () => {
95 expect(os.platform()).to.equal('foobar')
96 })
97
98 fancy
99 .stub(os, 'platform', sinon.stub().returns('foobar'))
100 .it('uses sinon', () => {
101 expect(os.platform()).to.equal('foobar')
102 expect(os.platform.called).to.equal(true)
103 })
104})
105```
106
107Catch
108-----
109
110catch errors in a declarative way. By default, ensures they are actually thrown as well.
111
112```js
113describe('catch tests', () => {
114 fancy
115 .do(() => { throw new Error('foobar') })
116 .catch(/foo/)
117 .it('uses regex')
118
119 fancy
120 .do(() => { throw new Error('foobar') })
121 .catch('foobar')
122 .it('uses string')
123
124 fancy
125 .do(() => { throw new Error('foobar') })
126 .catch(err => expect(err.message).to.match(/foo/))
127 .it('uses function')
128
129 fancy
130 // this would normally raise because there is no error being thrown
131 .catch('foobar', {raiseIfNotThrown: false})
132 .it('do not error if not thrown')
133})
134```
135
136Without fancy, you could check an error like this:
137
138```js
139it('dont do this', () => {
140 try {
141 myfunc()
142 } catch (err) {
143 expect(err.message).to.match(/my custom errorr/)
144 }
145})
146```
147
148But this has a common flaw, if the test does not error, the test will still pass. Chai and other assertion libraries have helpers for this, but they still end up with somewhat messy code.
149
150Finally
151-------
152
153Run a task even if the test errors out.
154
155```js
156describe('finally tests', () => {
157 fancy
158 .do(() => { throw new Error('x') })
159 .finally(() => { /* always called */ })
160 .end('always calls finally')
161})
162```
163
164Nock
165----
166
167Uses [nock](https://github.com/node-nock/nock) to mock out HTTP calls to external APIs. You'll need to also install nock in your `devDependencies`.
168Automatically calls `done()` to ensure the calls were made and `cleanAll()` to remove any pending requests.
169
170```js
171const fancy = require('fancy-test')
172
173describe('nock tests', () => {
174 fancy
175 .nock('https://api.github.com', api => api
176 .get('/me')
177 .reply(200, {name: 'jdxcode'})
178 )
179 .it('mocks http call to github', async () => {
180 const {body: user} = await HTTP.get('https://api.github.com/me')
181 expect(user).to.have.property('name', 'jdxcode')
182 })
183})
184```
185
186Environment Variables
187---------------------
188
189Sometimes it's helpful to clear out environment variables before running tests or override them to something common.
190
191```js
192describe('env tests', () => {
193 fancy
194 .env({FOO: 'BAR'})
195 .it('mocks FOO', () => {
196 expect(process.env.FOO).to.equal('BAR')
197 expect(process.env).to.not.deep.equal({FOO: 'BAR'})
198 })
199
200 fancy
201 .env({FOO: 'BAR'}, {clear: true})
202 .it('clears all env vars', () => {
203 expect(process.env).to.deep.equal({FOO: 'BAR'})
204 })
205})
206```
207
208Do
209---
210
211Run some arbitrary code within the pipeline. Useful to create custom logic and debugging.
212
213```js
214describe('run', () => {
215 fancy
216 .stdout()
217 .do(() => console.log('foo'))
218 .do(({stdout}) => expect(stdout).to.equal('foo\n'))
219 .it('runs this callback last', () => {
220 // test code
221 })
222
223 // add to context object
224 fancy
225 .add('a', () => 1)
226 .add('b', () => 2)
227 // context will be {a: 1, b: 2}
228 .it('does something with context', context => {
229 // test code
230 })
231})
232```
233
234Add
235---
236
237Similar to run, but extends the context object with a new property.
238Can return a promise or not.
239
240```js
241describe('add', () => {
242 fancy
243 .add('foo', () => 'foo')
244 .add('bar', () => Promise.resolve('bar'))
245 .do(ctx => expect(ctx).to.include({foo: 'foo', bar: 'bar'}))
246 .it('adds the properties')
247})
248```
249
250Stdin Mocking
251-------------
252
253Mocks stdin. You may have to pass a delay to have it wait a bit until it sends the event.
254
255```js
256describe('stdin test', () => {
257 fancy
258 .stdin('whoa there!\n')
259 .stdout()
260 .it('mocks', () => {
261 process.stdin.setEncoding('utf8')
262 process.stdin.once('data', data => {
263 // data === 'whoa there!\n'
264 })
265 })
266})
267```
268
269Stdout/Stderr Mocking
270---------------------
271
272This is used for tests that ensure that certain stdout/stderr messages are made.
273By default this also trims the output from the screen. See the output by setting `TEST_OUTPUT=1`, or by setting `{print: true}` in the options passed.
274
275You can use the library [stdout-stderr](https://npm.im/stdout-stderr) directly for doing this, but you have to be careful to always reset it after the tests run. We do that work for you so you don't have to worry about mocha's output being hidden.
276
277```js
278describe('stdmock tests', () => {
279 fancy
280 .stdout()
281 .it('mocks stdout', output => {
282 console.log('foobar')
283 expect(output.stdout).to.equal('foobar\n')
284 })
285
286 fancy
287 .stderr()
288 .it('mocks stderr', output => {
289 console.error('foobar')
290 expect(output.stderr).to.equal('foobar\n')
291 })
292
293 fancy
294 .stdout()
295 .stderr()
296 .it('mocks stdout and stderr', output => {
297 console.log('foo')
298 console.error('bar')
299 expect(output.stdout).to.equal('foo\n')
300 expect(output.stderr).to.equal('bar\n')
301 })
302})
303```
304
305Done
306----
307
308You can get the mocha `done()` callback by passing in a second argument.
309
310```js
311describe('calls done', () => {
312 fancy
313 .it('expects FOO=bar', (_, done) => {
314 done()
315 })
316})
317```
318
319Retries
320-------
321
322Retry the test n times.
323
324```js
325let count = 3
326
327describe('test retries', () => {
328 fancy
329 .retries(2)
330 .do(() => {
331 count--
332 if (count > 0) throw new Error('x')
333 })
334 .it('retries 3 times')
335})
336```
337
338Timeout
339-------
340
341Set mocha timeout duration.
342
343```js
344const wait = (ms = 10) => new Promise(resolve => setTimeout(resolve, ms))
345
346describe('timeout', () => {
347 fancy
348 .timeout(50)
349 .it('times out after 50ms', async () => {
350 await wait(100)
351 })
352})
353```
354
355Chai
356----
357
358This library includes [chai](https://npm.im/chai) for convenience:
359
360```js
361import {expect, fancy} from 'fancy-test'
362
363describe('has chai', () => {
364 fancy
365 .env({FOO: 'BAR'})
366 .it('expects FOO=bar', () => {
367 expect(process.env.FOO).to.equal('BAR')
368 })
369})
370```
371
372Chaining
373========
374
375Everything here is chainable. You can also store parts of a chain to re-use later on.
376
377For example:
378
379```js
380describe('my suite', () => {
381 let setupDB = fancy
382 .do(() => setupDB())
383 .env({FOO: 'FOO'})
384
385 setupDB
386 .stdout()
387 .it('tests with stdout mocked', () => {
388 // test code
389 })
390
391 setupDB
392 .env({BAR: 'BAR'})
393 .it('also mocks the BAR environment variable', () => {
394 // test code
395 })
396})
397```
398
399Using [do](#do) you can really maximize this ability. In fact, you don't even need to pass a callback to it if you prefer this syntax:
400
401```js
402describe('my suite', () => {
403 let setupDB = fancy
404 .do(() => setupDB())
405 .catch(/spurious db error/)
406 .do(() => setupDeps())
407
408 let testMyApp = testInfo => {
409 return setupDB.run()
410 .do(context => myApp(testInfo, context))
411 }
412
413 testMyApp({info: 'test run a'})
414 .it('tests a')
415
416 testMyApp({info: 'test run b'})
417 .it('tests b')
418})
419```
420
421Custom Plugins
422==============
423
424It's easy to create your own plugins to extend fancy. In [oclif](https://github.com/oclif/oclif) we use fancy to create [custom command testers](https://github.com/oclif/example-multi-ts/blob/master/test/commands/hello.test.ts).
425
426Here is an example that creates a counter that could be used to label each test run. See the [actual test](test/base.test.ts) to see the TypeScript types needed.
427
428```js
429let count = 0
430
431fancy = fancy
432.register('count', prefix => {
433 return {
434 run(ctx) {
435 ctx.count = ++count
436 ctx.testLabel = `${prefix}${count}`
437 }
438 }
439})
440
441describe('register', () => {
442 fancy
443 .count('test-')
444 .it('is test #1', context => {
445 expect(context.count).to.equal(1)
446 expect(context.testLabel).to.equal('test-1')
447 })
448
449 fancy
450 .count('test-')
451 .it('is test #2', context => {
452 expect(context.count).to.equal(2)
453 expect(context.testLabel).to.equal('test-2')
454 })
455})
456```
457
458TypeScript
459==========
460
461This module is built in typescript and exports the typings. Doing something with dynamic chaining like this was [not easy](src/base.ts), but it should be fully typed throughout. Look at the internal plugins to get an idea of how to keep typings for your custom plugins.