UNPKG

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