UNPKG

19.4 kBMarkdownView Raw
1# zora
2
3Fast javascript test runner 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 install --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### Tests are regular Javascript programs.
24
25You 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.
26If you have the following test.
27```Javascript
28import {test} from 'path/to/zora';
29
30test('should result to the answer', t => {
31 const answer = 42
32 t.equal(answer, 42, 'answer should be 42');
33});
34```
35
36You can run your test with
371. Node: ``node ./myTestFile.js``
382. In the browser ``<script type="module" src="./myTestFile.js></script>`` identically
39
40Moreover zora does not use specific platform API which should make it transparent to most of your tools such module bundlers or transpilers.
41
42In few words:
43> Zora is Ecmascript, no less, no more.
44
45### Tests are fast
46
47Tests 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.
48Zora is by far the **fastest** Javascript test runner in the ecosystem.
49
50#### Benchmark
51
52This repository includes a benchmark which consists on running N test files, with M tests in each and where one test lasts T milliseconds.
53About 5% of tests should fail.
54
551. profile library: N = 5, M = 8, T = 25ms
562. profile web app: N = 10, M = 8, T = 40ms
573. profile api: N =12, M = 10, T = 100ms
58
59Each framework runs with its default settings.
60
61Here are the result of different test frameworks on my developer machine (MacBook Pro, 2.7GH i5) with node 12 :
62
63| | zora@3.0.1 | tape@4.10.2 | Jest@24.8.0 | AvA@2.1.0 | Mocha@6.1.4|
64|--------|:------------:|:-----------: |:-------------:|:------------:|:----------:|
65|Library | 121ms | 1244ms | 2580ms | 1878ms | 1589ms |
66|Web app | 132ms | 3549ms | 3891ms | 2635ms | 3919ms |
67|API | 200ms | 12595ms | 6674ms | 3179ms | 12898ms |
68
69Of course as any benchmark, it may not cover your use case and you should probably run your own tests before you draw any conclusion.
70
71### Focus on tests only
72
73zora does one thing but hopefully does it well: **test**.
74
75In my opinions:
761. Pretty reporting (I have not said *efficient reporting*) should be handled by a specific tool.
772. Transpilation and other code transformation should be handled by a specific tool.
783. File watching and caching should be handled by a specific tool.
794. File serving should be handled by a specific tool.
805. Coffee should be made by a specific tool.
81
82As a result zora is much smaller of an install according to [packagephobia](https://packagephobia.now.sh) than all the others test frameworks
83
84| | zora@3.0.1 | tape@4.10.2 | Jest@24.8.2 | AvA@2.1.0 | Mocha@6.1.4|
85|--------|:------------:|:-----------:|:-------------:|:------------:|------------:|
86|Install size | [155kb](https://packagephobia.now.sh/result?p=zora) | [1004kb](https://packagephobia.now.sh/result?p=tape) | [38.6mb](https://packagephobia.now.sh/result?p=jest) | [15.8mb](https://packagephobia.now.sh/result?p=ava) | [5.79mb](https://packagephobia.now.sh/result?p=mocha)|
87
88### Reporter is handled with other process (TAP aware)
89
90When 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.
91Whether you want it to be printed in red, yellow etc is a matter of preference.
92
93For 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.
94
95## Usage
96
97### Basics
98
99You can use the top level assertion methods
100
101```Javascript
102import {equal, ok, isNot} from 'zora';
103
104ok(true,'true is truthy');
105
106equal('bar','bar', 'that both string are equivalent');
107
108isNot({},{},'those are not the same reference');
109
110//etc
111```
112
113If you run the previous program, test report will start on its own by default with the following console output:
114
115```TAP
116TAP version 13
117ok 1 - true is truthy
118ok 2 - that both string are equivalent
119ok 3 - those are not the same reference
1201..3
121
122# ok
123# success: 3
124# skipped: 0
125# failure: 0
126```
127
128However one will usually want to group assertions within a sub test: the ``test`` method can be used.
129
130```Javascript
131import {test} from 'zora';
132
133test('some grouped assertions', t => {
134 t.ok(true, 'true is truthy');
135 t.equal('bar', 'bar', 'that both string are equivalent');
136 t.isNot({}, {}, 'those are not the same reference');
137});
138```
139
140with the following result
141
142```TAP
143TAP version 13
144# some grouped assertions
145ok 1 - true is truthy
146ok 2 - that both string are equivalent
147ok 3 - those are not the same reference
1481..3
149
150# ok
151# success: 3
152# skipped: 0
153# failure: 0
154```
155
156You can also group tests within a parent test:
157
158```Javascript
159import {test} from 'zora';
160
161test('some grouped assertions', t => {
162 t.ok(true, 'true is truthy');
163
164 t.test('a group inside another one', t=>{
165 t.equal('bar', 'bar', 'that both string are equivalent');
166 t.isNot({}, {}, 'those are not the same reference');
167 });
168});
169```
170
171```TAP
172TAP version 13
173# some grouped assertions
174ok 1 - true is truthy
175# a group inside another one
176ok 2 - that both string are equivalent
177ok 3 - those are not the same reference
1781..3
179
180# ok
181# success: 3
182# skipped: 0
183# failure: 0
184```
185
186### Asynchronous tests and control flow
187
188Asynchronous tests are simply handled with async function:
189
190```Javascript
191test('with getUsers an asynchronous function returning a Promise',async t => {
192 const users = await getUsers();
193 t.eq(users.length, 2,'we should have 2 users');
194});
195```
196
197Notice that each test runs in its own micro task in parallel (for performance). It implies your tests should not depend on each other.
198It is often a good practice!
199However, 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).
200
201The 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
202
203```Javascript
204let state = 0;
205
206test('test 1', t => {
207 t.ok(true);
208 state++;
209});
210
211test('test 2', t => {
212 //Maybe yes maybe no, you have no guarantee ! In this case it will work as everything is sync
213 t.equal(state, 1);
214});
215
216//Same thing here even in nested tests
217test('grouped', t => {
218 let state = 0;
219
220 t.test('test 1', t => {
221 t.ok(true);
222 state++;
223 });
224
225 t.test('test 2', t => {
226 //Maybe yes maybe no, you have no guarantee ! In this case it will work as everything is sync
227 t.equal(state, 1);
228 });
229});
230
231//And
232test('grouped', t=>{
233 let state = 0;
234
235 t.test('test 1', async t=>{
236 t.ok(true);
237 await wait(100);
238 state++;
239 });
240
241 test('test 2', t=>{
242 t.equal(state, 0, 'see the old state value as it will have started to run before test 1 is done');
243 });
244});
245
246//But
247test('grouped', async t => {
248 let state = 0;
249
250 //specifically wait the end of this test before continuing !
251 await t.test('test 1', async t => {
252 t.ok(true);
253 await wait(100);
254 state++;
255 });
256
257 test('test 2', t => {
258 t.equal(state, 1, 'see the updated value!');
259 });
260});
261```
262
263### Changing TAP format
264
265TAP 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.
266In 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.
267In the same way these aforementioned tools expect diagnostics with a ``expected``, ``actual``, etc properties
268It is the one we have used in our previous examples.
269
270If you run the following program
271```Javascript
272import {test} from 'zora';
273
274test('tester 1', t => {
275
276 t.ok(true, 'assert1');
277
278 t.test('some nested tester', t => {
279 t.ok(true, 'nested 1');
280 t.ok(true, 'nested 2');
281 });
282
283 t.test('some nested tester bis', t => {
284 t.ok(true, 'nested 1');
285
286 t.test('deeply nested', t => {
287 t.ok(true, 'deeply nested really');
288 t.ok(true, 'deeply nested again');
289 });
290
291 t.notOk(true, 'nested 2'); // This one will fail
292 });
293
294 t.ok(true, 'assert2');
295});
296
297test('tester 2', t => {
298 t.ok(true, 'assert3');
299
300 t.test('nested in two', t => {
301 t.ok(true, 'still happy');
302 });
303
304 t.ok(true, 'assert4');
305});
306```
307
308You will see in the console
309```TAP
310TAP version 13
311# tester 1
312ok 1 - assert1
313# some nested tester
314ok 2 - nested 1
315ok 3 - nested 2
316# some nested tester bis
317ok 4 - nested 1
318# deeply nested
319ok 5 - deeply nested really
320ok 6 - deeply nested again
321not ok 7 - nested 2
322 ---
323 pass: false
324 actual: true
325 expected: "falsy value"
326 description: "nested 2"
327 operator: "notOk"
328 at: " t.test.t (/Volumes/Data/code/zora/test/samples/cases/nested.js:20:11)"
329 id: 7
330 ...
331ok 8 - assert2
332# tester 2
333ok 9 - assert3
334# nested in two
335ok 10 - still happy
336ok 11 - assert4
3371..11
338
339# not ok
340# success: 10
341# skipped: 0
342# failure: 1
343```
344
345Another 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
346do 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
347of the format.
348
349![tap output in a BDD format](./media/bsd.png)
350
351If you call the ``indent`` method on the ``test`` function anywhere in your test the output stream will indent sub tests and use other properties for diagnostic:
352
353```Javascript
354const {test} = require('zora.js');
355
356test.indent(); // INDENT
357
358test('tester 1', t => {
359
360 t.ok(true, 'assert1');
361
362 t.test('some nested tester', t => {
363 t.ok(true, 'nested 1');
364 t.ok(true, 'nested 2');
365 });
366
367 t.test('some nested tester bis', t => {
368 t.ok(true, 'nested 1');
369
370 t.test('deeply nested', t => {
371 t.ok(true, 'deeply nested really');
372 t.ok(true, 'deeply nested again');
373 });
374
375 t.notOk(true, 'nested 2'); // This one will fail
376 });
377
378 t.ok(true, 'assert2');
379});
380
381test('tester 2', t => {
382 t.ok(true, 'assert3');
383
384 t.test('nested in two', t => {
385 t.ok(true, 'still happy');
386 });
387
388 t.ok(true, 'assert4');
389});
390```
391
392will give you the following result
393```TAP
394TAP version 13
395# Subtest: tester 1
396 ok 1 - assert1
397 # Subtest: some nested tester
398 ok 1 - nested 1
399 ok 2 - nested 2
400 1..2
401 ok 2 - some nested tester # 1ms
402 # Subtest: some nested tester bis
403 ok 1 - nested 1
404 # Subtest: deeply nested
405 ok 1 - deeply nested really
406 ok 2 - deeply nested again
407 1..2
408 ok 2 - deeply nested # 1ms
409 not ok 3 - nested 2
410 ---
411 wanted: "falsy value"
412 found: true
413 at: " t.test.t (/Volumes/Data/code/zora/test/samples/cases/nested.js:22:11)"
414 operator: "notOk"
415 ...
416 1..3
417 not ok 3 - some nested tester bis # 1ms
418 ok 4 - assert2
419 1..4
420not ok 1 - tester 1 # 1ms
421# Subtest: tester 2
422 ok 1 - assert3
423 # Subtest: nested in two
424 ok 1 - still happy
425 1..1
426 ok 2 - nested in two # 0ms
427 ok 3 - assert4
428 1..3
429ok 2 - tester 2 # 0ms
4301..2
431
432# not ok
433# success: 10
434# skipped: 0
435# failure: 1
436```
437
438### Skip a test
439
440You 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
441and each skipped test will have a tap skip directive.
442
443```Javascript
444import {ok, skip, test} from 'zora';
445
446ok(true, 'hey hey');
447ok(true, 'hey hey bis');
448
449test('hello world', t => {
450 t.ok(true);
451 t.skip('blah', t => {
452 t.ok(false);
453 });
454 t.skip('for some reason');
455});
456
457skip('failing text', t => {
458 t.ok(false);
459});
460```
461
462```TAP
463TAP version 13
464ok 1 - hey hey
465ok 2 - hey hey bis
466# hello world
467ok 3 - should be truthy
468# blah
469ok 4 - blah # SKIP
470# for some reason
471ok 5 - for some reason # SKIP
472# failing text
473ok 6 - failing text # SKIP
4741..6
475
476# ok
477# success: 3
478# skipped: 3
479# failure: 0
480```
481
482### Assertion API
483
484- equal<T>(actual: T, expected: T, message?: string) verify if two values/instances are equivalent. It is often described as *deepEqual* in assertion libraries.
485aliases: eq, equals, deepEqual
486- notEqual<T>(actual: T, expected: T, message?: string) opposite of equal.
487aliases: notEquals, notEq, notDeepEqual
488- is<T>(actual: T, expected: T, message ?: string) verify whether two instances are the same (basically it is Object.is)
489aliases: same
490- isNot<T>(actual: T, expected: T, message ?: string)
491aliases: notSame
492- ok<T>(actual: T, message?: string) verify whether a value is truthy
493aliases: truthy
494- notOk<T>(actual: T, message?:string) verify whether a value is falsy
495aliases: falsy
496- fail(message?:string) an always failing test, usually when you want a branch of code not to be traversed
497- 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
498- 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
499
500### Create manually a test harness
501
502You can discard the default test harness and create your own. This has various effect:
503- the reporting won't start automatically, you will have to trigger it yourself but it also lets you know when the reporting is over
504- 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
505you can directly pass your custom reporter to transform the raw messages stream.
506
507```Javascript
508const {createHarness, mochaTapLike} = require('zora');
509
510const harness = createHarness();
511const {test} = harness;
512
513test('a first sub test', t => {
514 t.ok(true);
515
516 t.test('inside', t => {
517 t.ok(true);
518 });
519});
520
521test('a first sub test', t => {
522 t.ok(true);
523
524 t.test('inside', t => {
525 t.ok(false, 'oh no!');
526 });
527});
528
529harness
530 .report(mochaTapLike) // we have passed the mochaTapLike (with indention but here you can pass whatever you want
531 .then(() => {
532 // reporting is over: we can release some pending resources
533 console.log('DONE !');
534 // or in this case, our test program is for node so we want to set the exit code ourselves in case of failing test.
535 const exitCode = harness.pass === true ? 0 : 1;
536 process.exit(exitCode);
537 });
538```
539
540In practice you won't use this method unless you have specific requirements or want to build your own test runner on top of zora.
541
542## In the browser
543
544Zora itself does not depend on native Nodejs modules (such file system, processes, etc) so the code you will get is regular EcmaScript.
545
546### drop in file
547You can simply drop the dist file in the browser and write your script below (or load it).
548You can for example play with this [codepen](https://codepen.io/lorenzofox3/pen/YBWJrJ)
549
550```Html
551<!-- some content -->
552<body>
553<script type="module">
554
555import test from 'path/to/zora';
556
557test('some test', (assert) => {
558 assert.ok(true, 'hey there');
559})
560
561test('some failing test', (assert) => {
562 assert.fail('it failed');
563})
564</script>
565</body>
566<!-- some content -->
567```
568
569### As part of CI (example with rollup)
570
571I 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.
572
573assuming you have your entry point as follow :
574```Javascript
575//./test/index.js
576import test1 from './test1.js'; // some tests here
577import test2 from './test2.js'; // some more tests there
578import test3 from './test3.js'; // another test plan
579```
580
581where for example ./test/test1.js is
582```Javascript
583import test from 'zora';
584
585test('mytest', (assertions) => {
586 assertions.ok(true);
587})
588
589test('mytest', (assertions) => {
590 assertions.ok(true);
591});
592```
593you can then bundle your test as single program.
594
595```Javascript
596const node = require('rollup-plugin-node-resolve');
597const commonjs = require('rollup-plugin-commonjs');
598module.exports = {
599 input: './test/index.js',
600 output: [{
601 name: 'test',
602 format: 'iife',
603 sourcemap: 'inline' // ideal to debug
604 }],
605 plugins: [node(), commonjs()], //you can add babel plugin if you need transpilation
606};
607```
608
609You can now drop the result into a debug file
610``rollup -c path/to/conf > debug.js``
611
612And read with your browser (from an html document for example).
613
614![tap output in the browser console](./media/console-sc.png)
615
616Even 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.
617
618so all together, in your package.json you can have something like that
619```Javascript
620{
621// ...
622 "scripts": {
623 "test:ci": "rollup -c path/to/conf | tape-run"
624 }
625// ...
626}
627```
628
629## On exit codes
630
631Whether 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.
632Often 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.
633Hence you'll need to pipe zora output into one of those reporters to avoid false positive on your CI platform.
634
635For example, one of package.json script can be
636``"test:ci": npm test | tap-set-exit``