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