UNPKG

23 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
15If you are interested in a test runner for Nodejs, checkout [pta](https://github.com/lorenzofox3/zora-node) built on top of zora
16If you are interested in a test runner for the Browser environment, checkout [playwright-test](https://github.com/hugomrdias/playwright-test) which supports zora out of the box
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
23Interesting reading related to zora:
24* [Tools and the design of a testing experience](https://dev.to/lorenzofox3/tools-and-the-design-of-a-testing-experience-2mdc)
25* [High concurrency and performances in a testing experience](https://dev.to/lorenzofox3/there-is-beauty-in-simplicity-1npe)
26
27### Tests are regular Javascript programs.
28
29You 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.
30If you have the following test.
31```Javascript
32import {test} from 'path/to/zora';
33
34test('should result to the answer', t => {
35 const answer = 42;
36 t.equal(answer, 42, 'answer should be 42');
37});
38```
39
40You can run your test with
411. Node: ``node ./myTestFile.js``
422. In the browser ``<script type="module" src="./myTestFile.js></script>`` identically
43
44Moreover zora does not use specific platform API which should make it transparent to most of your tools such module bundlers or transpilers.
45
46In few words:
47> Zora is EcmaScript, no less, no more.
48
49### Tests are fast
50
51Tests 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.
52Zora is by far the **fastest** Javascript test runner in the ecosystem.
53
54#### Benchmark
55
56This repository includes a *pseudo benchmark* which consists on running N test files, with M tests in each and where one test lasts T milliseconds.
57About 5% of tests should fail.
58
591. profile library: N = 5, M = 8, T = 25ms
602. profile web app: N = 10, M = 8, T = 40ms
613. profile api: N =12, M = 10, T = 100ms
62
63Each framework runs with its default settings.
64
65Here are the result of different test frameworks on my developer machine (2,4 GHz Quad-Core Intel Core i5, 16GB RAM) with node 14 :
66
67| | zora@4.0.0 | pta@0.1.3 | tape@5.0.1 | Jest@26.5.2 | AvA@3.13.0 | Mocha@8.1.13| uvu@0.3.4
68|--------|:------------:|:------------:|:------------:|:-------------:|:------------:|:----------:|:----------:|
69|Library | 91ms | 177ms | 1232ms | 2440ms | 942ms | 1381ms | 1280ms |
70|Web app | 103ms | 213ms | 3575ms | 2826ms | 1369ms | 3714ms | 3599ms |
71|API | 169ms | 275ms | 12566ms | 4318ms | 1645ms | 12649ms | 12521ms |
72
73Of course as any benchmark, it may not cover your use case and you should probably run your own tests before you draw any conclusion.
74
75### Focus on tests only
76
77zora does one thing but hopefully does it well: **test**.
78
79In my opinions:
801. Pretty reporting (I have not said *efficient reporting*) should be handled by a specific tool.
812. Transpilation and other code transformation should be handled by a specific tool.
823. File watching and caching should be handled by a specific tool.
834. File serving should be handled by a specific tool.
845. Coffee should be made by a specific tool.
85
86As a result zora is much smaller of an install according to [packagephobia](https://packagephobia.now.sh) than all the others test frameworks
87
88| | zora | pta |tape | Jest | AvA | Mocha|
89|--------|:------------:|:------------:|:-----------:|:-------------:|:------------:|:------------:|
90|Install size | [![zora](https://packagephobia.now.sh/badge?p=zora)](https://packagephobia.now.sh/result?p=zora) | [![pta](https://packagephobia.now.sh/badge?p=pta)](https://packagephobia.now.sh/result?p=pta) | [![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) |
91
92### Reporting is handled with another process (TAP aware)
93
94When 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.
95Whether you want it to be printed in red, yellow etc is a matter of preference.
96
97For 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.
98
99## Usage
100
101### Basics
102
103You can use the top level assertion methods
104
105```Javascript
106import {equal, ok, isNot} from 'zora';
107
108ok(true,'true is truthy');
109
110equal('bar','bar', 'that both string are equivalent');
111
112isNot({},{},'those are not the same reference');
113
114//etc
115```
116
117If you run the previous program, test report will start on its own by default with the following console output:
118
119<details>
120 <summary>output.txt</summary>
121
122```TAP
123TAP version 13
124ok 1 - true is truthy
125ok 2 - that both string are equivalent
126ok 3 - those are not the same reference
1271..3
128
129# ok
130# success: 3
131# skipped: 0
132# failure: 0
133```
134
135</details>
136
137However one will usually want to group assertions within a sub test: the ``test`` method can be used.
138
139```Javascript
140import {test} from 'zora';
141
142test('some grouped assertions', t => {
143 t.ok(true, 'true is truthy');
144 t.equal('bar', 'bar', 'that both string are equivalent');
145 t.isNot({}, {}, 'those are not the same reference');
146});
147```
148
149with the following result
150
151<details>
152 <summary>output.txt</summary>
153
154```TAP
155TAP version 13
156# some grouped assertions
157ok 1 - true is truthy
158ok 2 - that both string are equivalent
159ok 3 - those are not the same reference
1601..3
161
162# ok
163# success: 3
164# skipped: 0
165# failure: 0
166```
167
168</details>
169
170You can also group tests within a parent test:
171
172```Javascript
173import {test} from 'zora';
174
175test('some grouped assertions', t => {
176 t.ok(true, 'true is truthy');
177
178 t.test('a group inside another one', t=>{
179 t.equal('bar', 'bar', 'that both string are equivalent');
180 t.isNot({}, {}, 'those are not the same reference');
181 });
182});
183```
184<details>
185 <summary>output.txt</summary>
186
187```TAP
188TAP version 13
189# some grouped assertions
190ok 1 - true is truthy
191# a group inside another one
192ok 2 - that both string are equivalent
193ok 3 - those are not the same reference
1941..3
195
196# ok
197# success: 3
198# skipped: 0
199# failure: 0
200```
201</details>
202
203### Asynchronous tests and control flow
204
205Asynchronous tests are simply handled with async function:
206
207```Javascript
208test('with getUsers an asynchronous function returning a Promise',async t => {
209 const users = await getUsers();
210 t.eq(users.length, 2,'we should have 2 users');
211});
212```
213
214Notice that each test runs in its own micro task in parallel (for performance). It implies your tests should not depend on each other.
215It is often a good practice!
216However, 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).
217
218The 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
219
220```Javascript
221let state = 0;
222
223test('test 1', t => {
224 t.ok(true);
225 state++;
226});
227
228test('test 2', t => {
229 //Maybe yes maybe no, you have no guarantee ! In this case it will work as everything is sync
230 t.equal(state, 1);
231});
232
233//Same thing here even in nested tests
234test('grouped', t => {
235 let state = 0;
236
237 t.test('test 1', t => {
238 t.ok(true);
239 state++;
240 });
241
242 t.test('test 2', t => {
243 //Maybe yes maybe no, you have no guarantee ! In this case it will work as everything is sync
244 t.equal(state, 1);
245 });
246});
247
248//And
249test('grouped', t=>{
250 let state = 0;
251
252 t.test('test 1', async t=>{
253 t.ok(true);
254 await wait(100);
255 state++;
256 });
257
258 test('test 2', t=>{
259 t.equal(state, 0, 'see the old state value as it will have started to run before test 1 is done');
260 });
261});
262
263//But
264test('grouped', async t => {
265 let state = 0;
266
267 //specifically wait the end of this test before continuing !
268 await t.test('test 1', async t => {
269 t.ok(true);
270 await wait(100);
271 state++;
272 });
273
274 test('test 2', t => {
275 t.equal(state, 1, 'see the updated value!');
276 });
277});
278```
279
280### Changing TAP format
281
282TAP 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.
283In 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.
284In the same way these aforementioned tools expect diagnostics with a ``expected``, ``actual``, etc properties
285It is the one we have used in our previous examples.
286
287If you run the following program
288```Javascript
289import {test} from 'zora';
290
291test('tester 1', t => {
292
293 t.ok(true, 'assert1');
294
295 t.test('some nested tester', t => {
296 t.ok(true, 'nested 1');
297 t.ok(true, 'nested 2');
298 });
299
300 t.test('some nested tester bis', t => {
301 t.ok(true, 'nested 1');
302
303 t.test('deeply nested', t => {
304 t.ok(true, 'deeply nested really');
305 t.ok(true, 'deeply nested again');
306 });
307
308 t.notOk(true, 'nested 2'); // This one will fail
309 });
310
311 t.ok(true, 'assert2');
312});
313
314test('tester 2', t => {
315 t.ok(true, 'assert3');
316
317 t.test('nested in two', t => {
318 t.ok(true, 'still happy');
319 });
320
321 t.ok(true, 'assert4');
322});
323```
324<details>
325 <summary>output.txt</summary>
326
327```TAP
328TAP version 13
329# tester 1
330ok 1 - assert1
331# some nested tester
332ok 2 - nested 1
333ok 3 - nested 2
334# some nested tester bis
335ok 4 - nested 1
336# deeply nested
337ok 5 - deeply nested really
338ok 6 - deeply nested again
339not ok 7 - nested 2
340 ---
341 actual: true
342 expected: "falsy value"
343 operator: "notOk"
344 at: " t.test.t (/Volumes/Data/code/zora/test/samples/cases/nested.js:20:11)"
345 ...
346ok 8 - assert2
347# tester 2
348ok 9 - assert3
349# nested in two
350ok 10 - still happy
351ok 11 - assert4
3521..11
353
354# not ok
355# success: 10
356# skipped: 0
357# failure: 1
358```
359
360</details>
361
362Another 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
363do 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
364of the format.
365
366![tap output in a BDD format](./media/bsd.png)
367
368You can ask zora to indent sub tests with configuration flag:
3691. setting node environment variable ``INDENT=true node ./path/to/test/program`` if you run the test program with node
3702. setting a global variable on the window object if you use the browser to run the test program
371```markup
372<script>INDENT=true;</script>
373<script src="path/to/test/program"></script>
374```
375
376```Javascript
377const {test} = require('zora.js');
378
379test('tester 1', t => {
380
381 t.ok(true, 'assert1');
382
383 t.test('some nested tester', t => {
384 t.ok(true, 'nested 1');
385 t.ok(true, 'nested 2');
386 });
387
388 t.test('some nested tester bis', t => {
389 t.ok(true, 'nested 1');
390
391 t.test('deeply nested', t => {
392 t.ok(true, 'deeply nested really');
393 t.ok(true, 'deeply nested again');
394 });
395
396 t.notOk(true, 'nested 2'); // This one will fail
397 });
398
399 t.ok(true, 'assert2');
400});
401
402test('tester 2', t => {
403 t.ok(true, 'assert3');
404
405 t.test('nested in two', t => {
406 t.ok(true, 'still happy');
407 });
408
409 t.ok(true, 'assert4');
410});
411```
412
413<details>
414 <summary>output.txt</summary>
415
416```TAP
417TAP version 13
418# Subtest: tester 1
419 ok 1 - assert1
420 # Subtest: some nested tester
421 ok 1 - nested 1
422 ok 2 - nested 2
423 1..2
424 ok 2 - some nested tester # 1ms
425 # Subtest: some nested tester bis
426 ok 1 - nested 1
427 # Subtest: deeply nested
428 ok 1 - deeply nested really
429 ok 2 - deeply nested again
430 1..2
431 ok 2 - deeply nested # 1ms
432 not ok 3 - nested 2
433 ---
434 wanted: "falsy value"
435 found: true
436 at: " t.test.t (/Volumes/Data/code/zora/test/samples/cases/nested.js:22:11)"
437 operator: "notOk"
438 ...
439 1..3
440 not ok 3 - some nested tester bis # 1ms
441 ok 4 - assert2
442 1..4
443not ok 1 - tester 1 # 1ms
444# Subtest: tester 2
445 ok 1 - assert3
446 # Subtest: nested in two
447 ok 1 - still happy
448 1..1
449 ok 2 - nested in two # 0ms
450 ok 3 - assert4
451 1..3
452ok 2 - tester 2 # 0ms
4531..2
454
455# not ok
456# success: 10
457# skipped: 0
458# failure: 1
459```
460
461</details>
462
463### Skip a test
464
465You 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
466and each skipped test will have a tap skip directive.
467
468```Javascript
469import {ok, skip, test} from 'zora';
470
471ok(true, 'hey hey');
472ok(true, 'hey hey bis');
473
474test('hello world', t => {
475 t.ok(true);
476 t.skip('blah', t => {
477 t.ok(false);
478 });
479 t.skip('for some reason');
480});
481
482skip('failing text', t => {
483 t.ok(false);
484});
485```
486
487<details>
488 <summary>output.txt</summary>
489
490```TAP
491TAP version 13
492ok 1 - hey hey
493ok 2 - hey hey bis
494# hello world
495ok 3 - should be truthy
496# blah
497ok 4 - blah # SKIP
498# for some reason
499ok 5 - for some reason # SKIP
500# failing text
501ok 6 - failing text # SKIP
5021..6
503
504# ok
505# success: 3
506# skipped: 3
507# failure: 0
508```
509
510</details>
511
512### Run only some tests
513
514While 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
515some sub tests, you will also have to call ``assertion.only`` to make a given sub test run.
516You will also have to set the ``RUN_ONLY`` flag to ``true`` (in the same way as ``INDENT``). ``only`` is a convenience
517for 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.
518
519```javascript
520test('should not run', t => {
521 t.fail('I should not run ');
522});
523
524only('should run', t => {
525 t.ok(true, 'I ran');
526
527 t.only('keep running', t => {
528 t.only('keeeeeep running', t => {
529 t.ok(true, ' I got there');
530 });
531 });
532
533 t.test('should not run', t => {
534 t.fail('shouldn ot run');
535 });
536});
537
538only('should run but nothing inside', t => {
539 t.test('will not run', t => {
540 t.fail('should not run');
541 });
542 t.test('will not run', t => {
543 t.fail('should not run');
544 });
545});
546```
547
548If you run the following program with node ``RUN_ONLY node ./path/to/program.js``, you will get the following output:
549
550<details>
551 <summary>output.txt</summary>
552
553```tap
554TAP version 13
555# should not run
556ok 1 - should not run # SKIP
557# should run
558ok 2 - I ran
559# keep running
560# keeeeeep running
561ok 3 - I got there
562# should not run
563ok 4 - should not run # SKIP
564# should run but nothing inside
565# will not run
566ok 5 - will not run # SKIP
567# will not run
568ok 6 - will not run # SKIP
5691..6
570
571# ok
572# success: 2
573# skipped: 4
574# failure: 0
575```
576
577</details>
578
579
580### Assertion API
581
582- equal<T>(actual: T, expected: T, message?: string) verify if two values/instances are equivalent. It is often described as *deepEqual* in assertion libraries.
583aliases: eq, equals, deepEqual
584- notEqual<T>(actual: T, expected: T, message?: string) opposite of equal.
585aliases: notEquals, notEq, notDeepEqual
586- is<T>(actual: T, expected: T, message ?: string) verify whether two instances are the same (basically it is Object.is)
587aliases: same
588- isNot<T>(actual: T, expected: T, message ?: string)
589aliases: notSame
590- ok<T>(actual: T, message?: string) verify whether a value is truthy
591aliases: truthy
592- notOk<T>(actual: T, message?:string) verify whether a value is falsy
593aliases: falsy
594- fail(message?:string) an always failing test, usually when you want a branch of code not to be traversed
595- 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
596- 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
597
598### Create manually a test harness
599
600You can discard the default test harness and create your own. This has various effects:
601- the reporting won't start automatically, you will have to trigger it yourself but it also lets you know when the reporting is over
602- 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
603you can directly pass your custom reporter to transform the raw messages stream.
604
605```Javascript
606const {createHarness} = require('zora');
607const {indentedTapReporter} = require('zora-tap-reporter');
608
609const harness = createHarness();
610const {test} = harness;
611
612test('a first sub test', t => {
613 t.ok(true);
614
615 t.test('inside', t => {
616 t.ok(true);
617 });
618});
619
620test('a first sub test', t => {
621 t.ok(true);
622
623 t.test('inside', t => {
624 t.ok(false, 'oh no!');
625 });
626});
627
628harness
629 .report(indentedTapReporter())
630 .then(() => {
631 // reporting is over: we can release some pending resources
632 console.log('DONE !');
633 // or in this case, our test program is for node so we want to set the exit code ourselves in case of failing test.
634 const exitCode = harness.pass === true ? 0 : 1;
635 process.exit(exitCode);
636 });
637```
638
639In practice you won't use this method unless you have specific requirements or want to build your own test runner on top of zora.
640
641## Nodejs test runner
642
643If you want a little bit more opiniated test runner based on zora you can check [pta](https://github.com/lorenzofox3/zora-node)
644
645## In the browser
646
647Zora itself does not depend on native Nodejs modules (such file system, processes, etc) so the code you will get is regular EcmaScript.
648
649### drop in file
650You can simply drop the dist file in the browser and write your script below (or load it).
651You can for example play with this [codepen](https://codepen.io/lorenzofox3/pen/YBWJrJ)
652
653```Html
654<!-- some content -->
655<body>
656<script type="module">
657
658import test from 'path/to/zora';
659
660test('some test', (assert) => {
661 assert.ok(true, 'hey there');
662})
663
664test('some failing test', (assert) => {
665 assert.fail('it failed');
666})
667</script>
668</body>
669<!-- some content -->
670```
671
672### Test runners
673
674There are few test runners which allow you to run testing programs in a browser environment (not emulated with JSDOM).
675* [playwright-test](https://karma-runner.github.io/4.0/index.html)
676* [Karma](https://karma-runner.github.io/4.0/index.html)
677* [zora-dev-server](https://github.com/lorenzofox3/zora-dev-server) - work in progress
678
679### As part of CI (example with rollup)
680
681I 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.
682
683assuming you have your entry point as follow :
684```Javascript
685//./test/index.js
686import test1 from './test1.js'; // some tests here
687import test2 from './test2.js'; // some more tests there
688import test3 from './test3.js'; // another test plan
689```
690
691where for example ./test/test1.js is
692```Javascript
693import test from 'zora';
694
695test('mytest', (assertions) => {
696 assertions.ok(true);
697})
698
699test('mytest', (assertions) => {
700 assertions.ok(true);
701});
702```
703you can then bundle your test as single program.
704
705```Javascript
706const node = require('rollup-plugin-node-resolve');
707const commonjs = require('rollup-plugin-commonjs');
708module.exports = {
709 input: './test/index.js',
710 output: [{
711 name: 'test',
712 format: 'iife',
713 sourcemap: 'inline' // ideal to debug
714 }],
715 plugins: [node(), commonjs()], //you can add babel plugin if you need transpilation
716};
717```
718
719You can now drop the result into a debug file
720``rollup -c path/to/conf > debug.js``
721
722And read with your browser (from an html document for example).
723
724![tap output in the browser console](./media/console-sc.png)
725
726Even 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.
727
728so all together, in your package.json you can have something like that
729```Javascript
730{
731// ...
732 "scripts": {
733 "test:ci": "rollup -c path/to/conf | tape-run"
734 }
735// ...
736}
737```
738
739## On exit codes
740
741Whether 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.
742Often 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.
743Hence you'll need to pipe zora output into one of those reporters to avoid false positive on your CI platform.
744
745For example, one of package.json script can be
746``"test:ci": npm test | tap-set-exit``
747
748## Contributing
749
7501. Clone the repository with git ``git https://github.com/lorenzofox3/zora.git`` (or from Github/Gitlab UI)
7512. install dependencies ``npm i``
7523. 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
7534. run the tests ``npm t``