UNPKG

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