UNPKG

21.9 kBMarkdownView Raw
1# zora
2
3Fast javascript testing library for **nodejs** and **browsers**
4
5[![CircleCI](https://badgen.net/circleci/github/lorenzofox3/zora)](https://circleci.com/gh/lorenzofox3/zora)
6[![npm](https://badgen.net/npm/v/zora)](https://www.npmjs.com/package/zora)
7[![install size](https://badgen.net/packagephobia/install/zora)](https://packagephobia.now.sh/result?p=zora)
8
9[Gitlab mirror](https://gitlab.com/zora-test/zora)
10
11## installation
12
13``npm i --save-dev zora``
14
15Note that the version 3 of zora targets modern Javascript engines. Behind the scene it uses *Asynchronous iterators* and *for await* statement. Both
16are supported by Node (>= 10 or >= 8 with flag) and all the major browsers. If you wish to use the v2 you can find its code and documentation on the [v2 branch](https://github.com/lorenzofox3/zora/tree/v2).
17
18## (Un)Opinions and Design
19
20These are the following rules and ideas I have followed while developing zora. Whether they are right or not is an entire different topic ! :D
21Note I have decided to develop zora specially because I was not able to find a tool which complies entirely with these ideas.
22
23[read more](https://dev.to/lorenzofox3/tools-and-the-design-of-a-testing-experience-2mdc) on how it fits in the [UNIX philosophy](https://en.wikipedia.org/wiki/Unix_philosophy)
24
25### Tests are regular Javascript programs.
26
27You don't need a specific test runner, a specific platform or any build step to run your `zora` tests. They are only regular valid EcmaScript 2018 programs.
28If you have the following test.
29```Javascript
30import {test} from 'path/to/zora';
31
32test('should result to the answer', t => {
33 const answer = 42
34 t.equal(answer, 42, 'answer should be 42');
35});
36```
37
38You can run your test with
391. Node: ``node ./myTestFile.js``
402. In the browser ``<script type="module" src="./myTestFile.js></script>`` identically
41
42Moreover zora does not use specific platform API which should make it transparent to most of your tools such module bundlers or transpilers.
43
44In few words:
45> Zora is EcmaScript, no less, no more.
46
47### Tests are fast
48
49Tests are part of our daily routine as software developers. Performance is part of the user experience and there is no reason you should wait seconds for your tests to run.
50Zora is by far the **fastest** Javascript test runner in the ecosystem.
51
52#### Benchmark
53
54This repository includes a benchmark which consists on running N test files, with M tests in each and where one test lasts T milliseconds.
55About 5% of tests should fail.
56
571. profile library: N = 5, M = 8, T = 25ms
582. profile web app: N = 10, M = 8, T = 40ms
593. profile api: N =12, M = 10, T = 100ms
60
61Each framework runs with its default settings.
62
63Here are the result of different test frameworks on my developer machine (MacBook Pro, 2.7GH i5) with node 12 :
64
65| | zora@3.1.0 | tape@4.11.2 | Jest@24.9.0 | AvA@2.4.0 | Mocha@6.2.1|
66|--------|:------------:|:-----------: |:-------------:|:------------:|:----------:|
67|Library | 102ms | 1240ms | 2835ms | 1888ms | 1349ms |
68|Web app | 134ms | 3523ms | 4084ms | 2900ms | 3696ms |
69|API | 187ms | 12586ms | 7380ms | 3900ms | 12766ms |
70
71Of course as any benchmark, it may not cover your use case and you should probably run your own tests before you draw any conclusion.
72
73### Focus on tests only
74
75zora does one thing but hopefully does it well: **test**.
76
77In my opinions:
781. Pretty reporting (I have not said *efficient reporting*) should be handled by a specific tool.
792. Transpilation and other code transformation should be handled by a specific tool.
803. File watching and caching should be handled by a specific tool.
814. File serving should be handled by a specific tool.
825. Coffee should be made by a specific tool.
83
84As a result zora is much smaller of an install according to [packagephobia](https://packagephobia.now.sh) than all the others test frameworks
85
86| | zora | tape | Jest | AvA | Mocha|
87|--------|:------------:|:-----------:|:-------------:|:------------:|:------------:|
88|Install size | [![zora](https://packagephobia.now.sh/badge?p=zora)](https://packagephobia.now.sh/result?p=zora) | [![tape](https://packagephobia.now.sh/badge?p=tape)](https://packagephobia.now.sh/result?p=tape) | [![jes](https://packagephobia.now.sh/badge?p=jest)](https://packagephobia.now.sh/result?p=jest) | [![ava](https://packagephobia.now.sh/badge?p=ava)](https://packagephobia.now.sh/result?p=ava) | [![mocha](https://packagephobia.now.sh/badge?p=mocha)](https://packagephobia.now.sh/result?p=mocha) |
89
90### Reporting is handled with another process (TAP aware)
91
92When you run a test you usually want to know whether there is any failure, where and why in order to debug and solve the issue as fast as possible.
93Whether you want it to be printed in red, yellow etc is a matter of preference.
94
95For this reason, zora output [TAP](http://testanything.org/) (Test Anything Protocol) by default. This protocol is "machine friendly" and widely used: [there are plenty of tools](https://github.com/sindresorhus/awesome-tap) to parse and deal with it the way **you** want.
96
97## Usage
98
99### Basics
100
101You can use the top level assertion methods
102
103```Javascript
104import {equal, ok, isNot} from 'zora';
105
106ok(true,'true is truthy');
107
108equal('bar','bar', 'that both string are equivalent');
109
110isNot({},{},'those are not the same reference');
111
112//etc
113```
114
115If you run the previous program, test report will start on its own by default with the following console output:
116
117```TAP
118TAP version 13
119ok 1 - true is truthy
120ok 2 - that both string are equivalent
121ok 3 - those are not the same reference
1221..3
123
124# ok
125# success: 3
126# skipped: 0
127# failure: 0
128```
129
130However one will usually want to group assertions within a sub test: the ``test`` method can be used.
131
132```Javascript
133import {test} from 'zora';
134
135test('some grouped assertions', t => {
136 t.ok(true, 'true is truthy');
137 t.equal('bar', 'bar', 'that both string are equivalent');
138 t.isNot({}, {}, 'those are not the same reference');
139});
140```
141
142with the following result
143
144```TAP
145TAP version 13
146# some grouped assertions
147ok 1 - true is truthy
148ok 2 - that both string are equivalent
149ok 3 - those are not the same reference
1501..3
151
152# ok
153# success: 3
154# skipped: 0
155# failure: 0
156```
157
158You can also group tests within a parent test:
159
160```Javascript
161import {test} from 'zora';
162
163test('some grouped assertions', t => {
164 t.ok(true, 'true is truthy');
165
166 t.test('a group inside another one', t=>{
167 t.equal('bar', 'bar', 'that both string are equivalent');
168 t.isNot({}, {}, 'those are not the same reference');
169 });
170});
171```
172
173```TAP
174TAP version 13
175# some grouped assertions
176ok 1 - true is truthy
177# a group inside another one
178ok 2 - that both string are equivalent
179ok 3 - those are not the same reference
1801..3
181
182# ok
183# success: 3
184# skipped: 0
185# failure: 0
186```
187
188### Asynchronous tests and control flow
189
190Asynchronous tests are simply handled with async function:
191
192```Javascript
193test('with getUsers an asynchronous function returning a Promise',async t => {
194 const users = await getUsers();
195 t.eq(users.length, 2,'we should have 2 users');
196});
197```
198
199Notice that each test runs in its own micro task in parallel (for performance). It implies your tests should not depend on each other.
200It is often a good practice!
201However, you'll be able to group your tests if you wish to conserve some state between them or wait one to finish before you start another one (ideal with tests running against real database).
202
203The sequence is simply controlled by AsyncFunction (and await keyboard), the ``test`` function return the result of its spec function argument, so you can control whether you want a specific test to complete before moving on
204
205```Javascript
206let state = 0;
207
208test('test 1', t => {
209 t.ok(true);
210 state++;
211});
212
213test('test 2', t => {
214 //Maybe yes maybe no, you have no guarantee ! In this case it will work as everything is sync
215 t.equal(state, 1);
216});
217
218//Same thing here even in nested tests
219test('grouped', t => {
220 let state = 0;
221
222 t.test('test 1', t => {
223 t.ok(true);
224 state++;
225 });
226
227 t.test('test 2', t => {
228 //Maybe yes maybe no, you have no guarantee ! In this case it will work as everything is sync
229 t.equal(state, 1);
230 });
231});
232
233//And
234test('grouped', t=>{
235 let state = 0;
236
237 t.test('test 1', async t=>{
238 t.ok(true);
239 await wait(100);
240 state++;
241 });
242
243 test('test 2', t=>{
244 t.equal(state, 0, 'see the old state value as it will have started to run before test 1 is done');
245 });
246});
247
248//But
249test('grouped', async t => {
250 let state = 0;
251
252 //specifically wait the end of this test before continuing !
253 await t.test('test 1', async t => {
254 t.ok(true);
255 await wait(100);
256 state++;
257 });
258
259 test('test 2', t => {
260 t.equal(state, 1, 'see the updated value!');
261 });
262});
263```
264
265### Changing TAP format
266
267TAP protocol is loosely defined in the sense that diagnostic is quite a free space and there is no well defined format to explicit a sub tests.
268In Javascript community most of the TAP parsers and tools were designed for [tape](https://github.com/substack/tape) which implies a TAP comment for a sub test header and every assertion is on the same level.
269In the same way these aforementioned tools expect diagnostics with a ``expected``, ``actual``, etc properties
270It is the one we have used in our previous examples.
271
272If you run the following program
273```Javascript
274import {test} from 'zora';
275
276test('tester 1', t => {
277
278 t.ok(true, 'assert1');
279
280 t.test('some nested tester', t => {
281 t.ok(true, 'nested 1');
282 t.ok(true, 'nested 2');
283 });
284
285 t.test('some nested tester bis', t => {
286 t.ok(true, 'nested 1');
287
288 t.test('deeply nested', t => {
289 t.ok(true, 'deeply nested really');
290 t.ok(true, 'deeply nested again');
291 });
292
293 t.notOk(true, 'nested 2'); // This one will fail
294 });
295
296 t.ok(true, 'assert2');
297});
298
299test('tester 2', t => {
300 t.ok(true, 'assert3');
301
302 t.test('nested in two', t => {
303 t.ok(true, 'still happy');
304 });
305
306 t.ok(true, 'assert4');
307});
308```
309
310You will see in the console
311```TAP
312TAP version 13
313# tester 1
314ok 1 - assert1
315# some nested tester
316ok 2 - nested 1
317ok 3 - nested 2
318# some nested tester bis
319ok 4 - nested 1
320# deeply nested
321ok 5 - deeply nested really
322ok 6 - deeply nested again
323not ok 7 - nested 2
324 ---
325 actual: true
326 expected: "falsy value"
327 operator: "notOk"
328 at: " t.test.t (/Volumes/Data/code/zora/test/samples/cases/nested.js:20:11)"
329 ...
330ok 8 - assert2
331# tester 2
332ok 9 - assert3
333# nested in two
334ok 10 - still happy
335ok 11 - assert4
3361..11
337
338# not ok
339# success: 10
340# skipped: 0
341# failure: 1
342```
343
344Another common structure is the one used by [node-tap](http://node-tap.org/). The structure can be parsed with common tap parser (such as [tap-parser](https://github.com/tapjs/tap-parser)) And will be parsed as well by tap parser which
345do not understand the indentation. However to take full advantage of the structure you should probably use a formatter (such [tap-mocha-reporter](https://www.npmjs.com/package/tap-mocha-reporter)) aware of this specific structure to get the whole benefit
346of the format.
347
348![tap output in a BDD format](./media/bsd.png)
349
350You can ask zora to indent sub tests with configuration flag:
3511. setting node environment variable ``INDENT=true node ./path/to/test/program`` if you run the test program with node
3522. setting a global variable on the window object if you use the browser to run the test program
353```markup
354<script>INDENT=true;</script>
355<script src="path/to/test/program></script>
356```
357
358```Javascript
359const {test} = require('zora.js');
360
361test('tester 1', t => {
362
363 t.ok(true, 'assert1');
364
365 t.test('some nested tester', t => {
366 t.ok(true, 'nested 1');
367 t.ok(true, 'nested 2');
368 });
369
370 t.test('some nested tester bis', t => {
371 t.ok(true, 'nested 1');
372
373 t.test('deeply nested', t => {
374 t.ok(true, 'deeply nested really');
375 t.ok(true, 'deeply nested again');
376 });
377
378 t.notOk(true, 'nested 2'); // This one will fail
379 });
380
381 t.ok(true, 'assert2');
382});
383
384test('tester 2', t => {
385 t.ok(true, 'assert3');
386
387 t.test('nested in two', t => {
388 t.ok(true, 'still happy');
389 });
390
391 t.ok(true, 'assert4');
392});
393```
394
395will give you the following result
396```TAP
397TAP version 13
398# Subtest: tester 1
399 ok 1 - assert1
400 # Subtest: some nested tester
401 ok 1 - nested 1
402 ok 2 - nested 2
403 1..2
404 ok 2 - some nested tester # 1ms
405 # Subtest: some nested tester bis
406 ok 1 - nested 1
407 # Subtest: deeply nested
408 ok 1 - deeply nested really
409 ok 2 - deeply nested again
410 1..2
411 ok 2 - deeply nested # 1ms
412 not ok 3 - nested 2
413 ---
414 wanted: "falsy value"
415 found: true
416 at: " t.test.t (/Volumes/Data/code/zora/test/samples/cases/nested.js:22:11)"
417 operator: "notOk"
418 ...
419 1..3
420 not ok 3 - some nested tester bis # 1ms
421 ok 4 - assert2
422 1..4
423not ok 1 - tester 1 # 1ms
424# Subtest: tester 2
425 ok 1 - assert3
426 # Subtest: nested in two
427 ok 1 - still happy
428 1..1
429 ok 2 - nested in two # 0ms
430 ok 3 - assert4
431 1..3
432ok 2 - tester 2 # 0ms
4331..2
434
435# not ok
436# success: 10
437# skipped: 0
438# failure: 1
439```
440
441### Skip a test
442
443You can decide to skip some tests if you wish not to run them, in that case they will be considered as _passing_. However the assertion summary at the end will tell you that some tests have been skipped
444and each skipped test will have a tap skip directive.
445
446```Javascript
447import {ok, skip, test} from 'zora';
448
449ok(true, 'hey hey');
450ok(true, 'hey hey bis');
451
452test('hello world', t => {
453 t.ok(true);
454 t.skip('blah', t => {
455 t.ok(false);
456 });
457 t.skip('for some reason');
458});
459
460skip('failing text', t => {
461 t.ok(false);
462});
463```
464
465```TAP
466TAP version 13
467ok 1 - hey hey
468ok 2 - hey hey bis
469# hello world
470ok 3 - should be truthy
471# blah
472ok 4 - blah # SKIP
473# for some reason
474ok 5 - for some reason # SKIP
475# failing text
476ok 6 - failing text # SKIP
4771..6
478
479# ok
480# success: 3
481# skipped: 3
482# failure: 0
483```
484
485### Run only some tests
486
487While developing, you may want to only run some tests. You can do so by using the ``only`` function. If the test you want to run has
488some sub tests, you will also have to call ``assertion.only`` to make a given sub test run.
489You will also have to set the ``RUN_ONLY`` flag to ``true`` (in the same way as ``INDENT``). ``only`` is a convenience
490for a developer while working, it has not real meaning for the testing program, so if you use only in the testing program and run it without the RUN_ONLY mode, it will bailout.
491
492```javascript
493test('should not run', t => {
494 t.fail('I should not run ');
495});
496
497only('should run', t => {
498 t.ok(true, 'I ran');
499
500 t.only('keep running', t => {
501 t.only('keeeeeep running', t => {
502 t.ok(true, ' I got there');
503 });
504 });
505
506 t.test('should not run', t => {
507 t.fail('shouldn ot run');
508 });
509});
510
511only('should run but nothing inside', t => {
512 t.test('will not run', t => {
513 t.fail('should not run');
514 });
515 t.test('will not run', t => {
516 t.fail('should not run');
517 });
518});
519```
520
521If you run the following program with node ``RUN_ONLY node ./path/to/program.js``, you will get the following output:
522
523```tap
524TAP version 13
525# should not run
526ok 1 - should not run # SKIP
527# should run
528ok 2 - I ran
529# keep running
530# keeeeeep running
531ok 3 - I got there
532# should not run
533ok 4 - should not run # SKIP
534# should run but nothing inside
535# will not run
536ok 5 - will not run # SKIP
537# will not run
538ok 6 - will not run # SKIP
5391..6
540
541# ok
542# success: 2
543# skipped: 4
544# failure: 0
545```
546
547### Assertion API
548
549- equal<T>(actual: T, expected: T, message?: string) verify if two values/instances are equivalent. It is often described as *deepEqual* in assertion libraries.
550aliases: eq, equals, deepEqual
551- notEqual<T>(actual: T, expected: T, message?: string) opposite of equal.
552aliases: notEquals, notEq, notDeepEqual
553- is<T>(actual: T, expected: T, message ?: string) verify whether two instances are the same (basically it is Object.is)
554aliases: same
555- isNot<T>(actual: T, expected: T, message ?: string)
556aliases: notSame
557- ok<T>(actual: T, message?: string) verify whether a value is truthy
558aliases: truthy
559- notOk<T>(actual: T, message?:string) verify whether a value is falsy
560aliases: falsy
561- fail(message?:string) an always failing test, usually when you want a branch of code not to be traversed
562- throws(fn: Function, expected?: string | RegExp | Function, description ?: string) expect an error to be thrown, you check the expected error by Regexp, Constructor or name
563- doesNotThrow(fn: Function, expected?: string | RegExp | Function, description ?: string) expect an error not to be thrown, you check the expected error by Regexp, Constructor or name
564
565### Create manually a test harness
566
567You can discard the default test harness and create your own. This has various effect:
568- the reporting won't start automatically, you will have to trigger it yourself but it also lets you know when the reporting is over
569- you can pass a custom reporter. Zora produces a stream of messages which are then transformed into a TAP stream. If you create the test harness yourself
570you can directly pass your custom reporter to transform the raw messages stream.
571
572```Javascript
573const {createHarness, mochaTapLike} = require('zora');
574
575const harness = createHarness();
576const {test} = harness;
577
578test('a first sub test', t => {
579 t.ok(true);
580
581 t.test('inside', t => {
582 t.ok(true);
583 });
584});
585
586test('a first sub test', t => {
587 t.ok(true);
588
589 t.test('inside', t => {
590 t.ok(false, 'oh no!');
591 });
592});
593
594harness
595 .report(mochaTapLike) // we have passed the mochaTapLike (with indention but here you can pass whatever you want
596 .then(() => {
597 // reporting is over: we can release some pending resources
598 console.log('DONE !');
599 // or in this case, our test program is for node so we want to set the exit code ourselves in case of failing test.
600 const exitCode = harness.pass === true ? 0 : 1;
601 process.exit(exitCode);
602 });
603```
604
605In practice you won't use this method unless you have specific requirements or want to build your own test runner on top of zora.
606
607## In the browser
608
609Zora itself does not depend on native Nodejs modules (such file system, processes, etc) so the code you will get is regular EcmaScript.
610
611### drop in file
612You can simply drop the dist file in the browser and write your script below (or load it).
613You can for example play with this [codepen](https://codepen.io/lorenzofox3/pen/YBWJrJ)
614
615```Html
616<!-- some content -->
617<body>
618<script type="module">
619
620import test from 'path/to/zora';
621
622test('some test', (assert) => {
623 assert.ok(true, 'hey there');
624})
625
626test('some failing test', (assert) => {
627 assert.fail('it failed');
628})
629</script>
630</body>
631<!-- some content -->
632```
633
634### As part of CI (example with rollup)
635
636I will use [rollup](http://rollupjs.org/) for this example, but you should not have any problem with [webpack](https://webpack.github.io/) or [browserify](http://browserify.org/). The idea is simply to create a test file your testing browsers will be able to run.
637
638assuming you have your entry point as follow :
639```Javascript
640//./test/index.js
641import test1 from './test1.js'; // some tests here
642import test2 from './test2.js'; // some more tests there
643import test3 from './test3.js'; // another test plan
644```
645
646where for example ./test/test1.js is
647```Javascript
648import test from 'zora';
649
650test('mytest', (assertions) => {
651 assertions.ok(true);
652})
653
654test('mytest', (assertions) => {
655 assertions.ok(true);
656});
657```
658you can then bundle your test as single program.
659
660```Javascript
661const node = require('rollup-plugin-node-resolve');
662const commonjs = require('rollup-plugin-commonjs');
663module.exports = {
664 input: './test/index.js',
665 output: [{
666 name: 'test',
667 format: 'iife',
668 sourcemap: 'inline' // ideal to debug
669 }],
670 plugins: [node(), commonjs()], //you can add babel plugin if you need transpilation
671};
672```
673
674You can now drop the result into a debug file
675``rollup -c path/to/conf > debug.js``
676
677And read with your browser (from an html document for example).
678
679![tap output in the browser console](./media/console-sc.png)
680
681Even better, you can use tap reporter browser friendly such [tape-run](https://www.npmjs.com/package/tape-run) so you'll have a proper exit code depending on the result of your tests.
682
683so all together, in your package.json you can have something like that
684```Javascript
685{
686// ...
687 "scripts": {
688 "test:ci": "rollup -c path/to/conf | tape-run"
689 }
690// ...
691}
692```
693
694## On exit codes
695
696Whether you have failing tests or not, unless you have an unexpected error, the process will return an exit code 0: zora considers its duty is to run the program to its end whether there is failing test or no.
697Often CI platforms require an exit code of 1 to mark a build as failed. That is not an issue, there are plenty of TAP reporters which when parsing a TAP stream will exit the process with code 1 if they encounter a failing test.
698Hence you'll need to pipe zora output into one of those reporters to avoid false positive on your CI platform.
699
700For example, one of package.json script can be
701``"test:ci": npm test | tap-set-exit``
702
703## Contributing
704
7051. Clone the repository with git ``git https://github.com/lorenzofox3/zora.git`` (or from Github/Gitlab UI)
7062. install dependencies ``npm i``
7073. build the source files ``npm run build``. Alternatively, if you are under "heavy" development you can run ``npm run dev`` it will build source files on every change
7084. run the tests ``npm t``