ZUnit
A zero dependency, non-polluting, low magic, test harness for Node.js
Install / Use
/learn @acuminous/ZUnitREADME
zUnit
TL;DR
zUnit = goodbits(tape) + goodbits(mocha) - dependencies;
Index
About
zUnit is a zero dependency<sup>1</sup>, non-polluting<sup>2</sup> test harness for Node.js that you can execute like any other JavaScript program. I wrote it because mocha, my preferred test harness, is the number one culprit for vulnerabilities in my open source projects and I'm tired of updating them just because mocha, or one of its dependencies triggered an audit warning. While zUnit does lack some of the advanced features, such as concurrent tests, automatic retries and true file globbing<sup>3</sup>, most of the day-to-day features are present.
1. zero-dependency
zUnit has no production dependencies, but does depend on a few development dependencies such as eslint and prettier.
2. non-polluting
You can add test functions (describe, it, etc) to the global namespace via the pollute config option.
3. advanced-features
Since writing zUnit I've begun to wonder whether some of Mocha's advanced features are universally beneficial. Many test suites are too small to warrant concurrency, and others (e.g. persistence tests) may require a great deal of effort to isolate. Concurrent testing also has drawbacks - the test harness and reporters become more complex and the output must be buffered, delaying feedback. I'm also unconvinced about automaticaly retrying tests, I think it better to fix any that are flakey, and take a statistical approach when results are naturally unpredictable.
Usage
-
Install zUnit
npm i zunit --save-dev -
Add the zUnit script to package.json
{ "scripts": { "test": "zUnit" } } -
Create a test suite, e.g.
test/user-db.test.jsconst { describe, it, xit, beforeEach } = require('zunit'); const assert = require('assert'); const userDb = require('../lib/user-db'); describe('User DB', () => { beforeEach(async () => { await userDb.flush(); }); describe('List Users', () => { it('should list all users', async () => { await userDb.create({ name: 'John' }); await userDb.create({ name: 'Julie' }); const users = await userDb.list(); assert.strictEqual(users.length, 2); assert.strictEqual(users[0].name, 'John'); assert.strictEqual(users[1].name, 'Julie'); }); xit('should list matching users', async () => {}); }); }); -
Run the tests
npm test User DB List Users should list all users - PASSED (2ms) should list matching use - SKIPPED (0ms) Summary Tests: 2, Passed: 1, Skipped: 1, Failed: 0, Duration: 2ms
Breaking Changes
4.0.0
Suite.discover() was made asynchronous to support dynmically importing ECMAScript modules. If you used a custom launch script that automatically discovers tests you will need to update it to wait for discover to resolve. Another breaking side-effect of this change is that it is no longer possible to implicitly export test suites. Previously you could require test suites that were defined with the describe syntax as follows...
describe('Database Tests', () => {
// ...
});
const databaseTests = require('./databaseTests.test.js');
const allTests = new Suite('All Tests').add(databaseTests);
Now if you manually compose test suites rather than discovering them, they must be explicitly exported, e.g.
module.exports = describe('Database Tests', () => {
// ...
});
Configuration
You can configure zUnit's launch script by:
- Specifying a configuration file when invoking the script, e.g.
{ "scripts": { "test": "zUnit test/zUnit.json" } } - Adding a
zUnitsubdocument to package.json, e.g.{ "zUnit": { "exit": true, "pollute": true, "require": ["test/setup.js"] } } - Creating a file called
.zUnit.jsonor.zUnit.jsin the project root
Configuration options
| Name | Type | Default | Notes |
| --------- | ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------ |
| name | String | package.name | The top level suite name. |
| directory | String | path.resolve('test') | The initial directory to recurse when requiring tests. |
| pattern | String or RegExp | /^[\w-]+\.test\.js$/ | The regular expression to use for matching test files. Omit the start and end slashes when using json |
| require | Array | [] | A list of CommonJS or ECMAScript modules to load before discovering tests. |
| pollute | Boolean | false | Control whether to pollute the global namespace with test functions so you don't have to require them. |
| exit | Boolean | false | For the node process to exit after tests are complete. |
Testing
Callbacks
Sometimes the code under test uses callbacks, making it easier if the test is callback based too. If you define your test functions to take two arguments, the second argument will be passed a callback which you should invoke to signify that the test is done. e.g.
const { describe, it } = require('zunit');
it('should do something wonderful', (test, done) => {
callbackApi((err, items) => {
if (err) return done(err);
assert.strictEqual(items.length, 0);
done();
});
});
Unlike with mocha, you can make the test function asynchronous, allowing you to use await when you have a mixture of callback and promise based code in your test.
Pending / Skipping Tests
You can define pending tests / skip tests in the following ways...
-
Using
xitconst { describe, xit } = require('zunit'); describe('My Suite', () => { xit('should do something wonderful', async () => { // ... }); }); -
Passing an option to
itconst { describe, it } = require('zunit'); describe('My Suite', () => { it( 'should do something wonderful', async () => { // ... }, { skip: true, reason: 'Optional Reason' } ); }); -
Using
xdescribeconst { xdescribe, it } = require('zunit'); xdescribe('My Suite', () => { it('should do something wonderful', async () => { // ... }); }); -
Passing an option to
describeconst { describe, it } = require('zunit'); describe( 'My Suite', () => { it('should do something wonderful', async () => { // ... }); }, { skip: true, reason: 'Optional Reason' } ); -
Defining a test without a test function
const { describe, it } = require('zunit'); describe('My Suite', () => { it('should do something wonderful'); }); -
Returning
test.skip()from within a test functionconst { describe, it } = require('zunit'); describe('My Suite', () => { it('should do something wonderful', async (test) => { return test.skip('Optional Reason'); }); }); -
In a beforeEach hook
const { describe, it, beforeEach } = require('zunit'); describe('My Suite', () => { beforeEach(async (hook) => { return hook.test.skip('Optional Reason'); }); it('should do something wonderful', async (test) => { // ... }); }); -
In a before hook
const { describe, it, before } = require('zunit'); describe('My Suite', () => { before(async (hook) => { return hook.suite.skip('Optional Reason'); }); it('should do something wond
Related Skills
node-connect
349.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.0kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
