# tape-six > A TAP-based unit testing library for modern JavaScript (ES6+). Works in Node, Deno, Bun, and browsers. Runs ES modules natively, supports TypeScript without transpilation. The npm package name is `tape-six` but all internal names use `tape6`. - Minimal, zero-dependency test library with a familiar API - Test files are directly executable: `node test.js`, `bun run test.js`, `deno run -A test.js` - Parallel test execution via worker threads - TAP, TTY (colored), and JSONL output formats - Browser testing with built-in web UI; headless automation via tape-six-puppeteer / tape-six-playwright - Before/after hooks: `beforeAll`, `afterAll`, `beforeEach`, `afterEach` - `test()` is aliased as `suite()`, `describe()`, and `it()` for easy migration - When called inside a test body, top-level functions auto-delegate to the current tester - BYO assertion / mocking / property-based libs: `node:assert`, `chai`, `expect`, `node:test` mock, `sinon`, `fast-check` ## Quick start Install: ```bash npm i -D tape-six ``` Write a test (`tests/test-example.js`): ```js import test from 'tape-six'; test('example', t => { t.ok(true, 'truthy'); t.equal(1 + 1, 2, 'math works'); t.deepEqual([1, 2], [1, 2], 'arrays match'); }); ``` Run it directly: ```bash node tests/test-example.js ``` Or run all configured tests: ```bash npx tape6 --flags FO ``` ## Importing ```js import test from 'tape-six'; // or: import {test} from 'tape-six'; // or: import {test, beforeAll, afterAll, beforeEach, afterEach} from 'tape-six'; // or: import {describe, it} from 'tape-six'; // CommonJS: // const {test} = require('tape-six'); ``` ## test() API `test` registers a test suite. All three arguments are optional and recognized by type: - `async test(name, options, testFn)` — registers a test suite. - `test.skip(name, options, testFn)` — registers a skipped test suite. - `test.todo(name, options, testFn)` — registers a TODO test suite (failures not counted). - `test.asPromise(name, options, testPromiseFn)` — registers a callback-style test: `testPromiseFn(tester, resolve, reject)`. Arguments: - `name` (string, optional) — test name. Defaults to function name or `'(anonymous)'`. - `options` (object, optional): - `name` — overridden by the `name` argument. - `skip` (boolean) — skip this test. - `todo` (boolean) — mark as TODO. - `timeout` (number, ms) — timeout for async tests. - `beforeAll`, `afterAll`, `beforeEach`, `afterEach` — hook functions. - `before` — alias for `beforeAll`. - `after` — alias for `afterAll`. - `testFn` (function) — `async testFn(tester)`. Can be sync or async. Flexible call signatures: ```js test(name, options, testFn); test(name, testFn); test(testFn); test(name, options); test(options, testFn); test(options); ``` Examples: ```js test('foo', t => { t.pass(); }); test('bar', async t => { const result = await fetchData(); t.equal(result.status, 200); }); test.skip('not ready yet', t => { t.fail(); }); test.todo('work in progress', t => { t.ok(false); // reported but not counted as failure }); test.asPromise('callback style', (t, resolve, reject) => { const stream = getStream(); stream.on('end', resolve); stream.on('error', reject); }); ``` ### Embedded tests ```js test('top', async t => { t.pass(); await t.test('nested', async t => { t.pass(); }); }); ``` Always `await` embedded tests to preserve execution order. Top-level `test()`/`it()`/`describe()` auto-delegate when called inside a test body, so these are equivalent: ```js import {describe, it, before, beforeEach} from 'tape-six'; describe('module', () => { before(() => { /* setup */ }); beforeEach(() => { /* per-test setup */ }); it('works', t => { t.ok(true); }); it('also works', t => { t.equal(1, 1); }); }); ``` ## Tester API The `Tester` object is passed to test functions. All `msg` arguments are optional. ### Properties - `signal` — `AbortSignal` triggered when the test is stopped or timed out. - `any` (alias `_`) — wildcard for deep equality matching. ### Assert methods - `pass(msg)` — assert pass. - `fail(msg)` — assert fail. - `ok(val, msg)` — assert truthy. Aliases: `true()`, `assert()`. - `notOk(val, msg)` — assert falsy. Aliases: `false()`, `notok()`. - `error(err, msg)` — assert err is falsy. Aliases: `ifError()`, `ifErr()`, `iferror()`. - `strictEqual(a, b, msg)` — assert `a === b`. Aliases: `is()`, `equal()`, `isEqual()`, `equals()`, `strictEquals()`. - `notStrictEqual(a, b, msg)` — assert `a !== b`. Aliases: `not()`, `notEqual()`, `notEquals()`, `notStrictEquals()`, `doesNotEqual()`, `isUnequal()`, `isNotEqual()`, `isNot()`. - `looseEqual(a, b, msg)` — assert `a == b`. Alias: `looseEquals()`. - `notLooseEqual(a, b, msg)` — assert `a != b`. Alias: `notLooseEquals()`. - `deepEqual(a, b, msg)` — deep strict equality. Aliases: `same()`, `deepEquals()`, `isEquivalent()`. - `notDeepEqual(a, b, msg)` — not deeply equal. Aliases: `notSame()`, `notDeepEquals()`, `notEquivalent()`, `notDeeply()`, `isNotDeepEqual()`, `isNotEquivalent()`. - `deepLooseEqual(a, b, msg)` — deep loose equality. - `notDeepLooseEqual(a, b, msg)` — not deeply loosely equal. - `throws(fn, msg)` — assert fn throws. - `doesNotThrow(fn, msg)` — assert fn does not throw. - `matchString(string, regexp, msg)` — assert string matches regexp. - `doesNotMatchString(string, regexp, msg)` — assert string does not match regexp. - `match(a, b, msg)` — deep structural match (supports wildcards). - `doesNotMatch(a, b, msg)` — assert no deep match. - `rejects(promise, msg)` — assert promise rejects (async, await it). Alias: `doesNotResolve()`. - `resolves(promise, msg)` — assert promise resolves (async, await it). Alias: `doesNotReject()`. ### Embedded test methods - `test(name, options, testFn)` — nested test suite (async, await it). Top-level `test()`/`it()` also works. - `skip(name, options, testFn)` — skip nested suite. - `todo(name, options, testFn)` — TODO nested suite. - `asPromise(name, options, testPromiseFn)` — callback-style nested suite. ### Hooks - `beforeAll(fn)` / `before(fn)` — run before first nested test. Top-level `beforeAll()`/`before()` also works. - `afterAll(fn)` / `after(fn)` — run after last nested test. Top-level `afterAll()`/`after()` also works. - `beforeEach(fn)` — run before each nested test. Top-level `beforeEach()` also works. - `afterEach(fn)` — run after each nested test. Top-level `afterEach()` also works. ### Miscellaneous - `plan(n)` — record expected number of direct assertions. On test end, emits a `# plan != count: expected N, ran M` TAP comment if the count diverges (diagnostic only, doesn't fail the test). Subtest assertions don't count toward the parent's plan. - `comment(msg)` — send a comment to the reporter. - `skipTest(...args, msg)` — skip current test with a message. - `bailOut(msg)` — abort the test suite. ### Expression evaluator - `OK(condition, msg, options)` — returns code string for `eval()`. Aliases: `TRUE()`, `ASSERT()`. - `condition`: a JavaScript expression as a string. - On failure, reports values of all variables in the expression. - `options.self`: tester variable name (default: `"t"`). ```js test('evaluator', t => { const a = 1, b = 2; eval(t.OK('a + b === 3')); }); ``` ## Before and after hooks Hooks are scoped — they only affect tests at their level. Top-level hooks (affect top-level tests only): ```js import {test, beforeAll, afterAll, beforeEach, afterEach} from 'tape-six'; beforeAll(() => { /* runs once before first top-level test */ }); afterAll(() => { /* runs once after last top-level test */ }); beforeEach(() => { /* runs before each top-level test */ }); afterEach(() => { /* runs after each top-level test */ }); ``` Nested hooks (top-level functions auto-delegate to current tester): ```js import {test, beforeEach, afterEach} from 'tape-six'; test('suite', async t => { beforeEach(() => { /* before each nested test */ }); afterEach(() => { /* after each nested test */ }); // equivalent: t.beforeEach(), t.afterEach() await t.test('test 1', t => t.pass()); await t.test('test 2', t => t.pass()); }); ``` Hooks via options (reusable): ```js const opts = { beforeEach: () => setupDb(), afterEach: () => teardownDb() }; test('suite', opts, async t => { await t.test('test 1', t => t.pass()); }); ``` Multiple hooks of the same type run in registration order (before) or reverse order (after). ## Plugins Tester methods can be added via `registerTesterMethod(name, fn)`. Same name + same fn → no-op (idempotent re-import); same name + different fn → throws (loud collision detection). ```js import {registerTesterMethod} from 'tape-six'; registerTesterMethod('spawnBin', async function (bin, args) { // ... implementation, can call this.equal(...), this.reporter.report({...}), etc. }); // then in tests: test('cli', async t => { const {code} = await t.spawnBin('node', ['-v']); t.equal(code, 0); }); ``` `Tester` is declared as an `interface` in `index.d.ts` to make TS module augmentation natural — plugins extend the interface, no full class shape needed. The built-in `t.OK()` evaluator (`src/OK.js`) uses this exact pattern. Plugin installation is **per-file**. `tape6-seq` runs all files in one process; `tape6` uses workers; `tape6-proc` (from the sister package `tape-six-proc`) uses subprocesses; browser tests run in iframes. Each isolation context has its own module graph, so a plugin import in file A is invisible to file B when they're in different contexts. Every test file that uses a plugin must import it directly. `registerTesterMethod`'s idempotency makes repeated imports safe. See [Writing plugins](https://github.com/uhop/tape-six/wiki/Writing-plugins) for the full guide. ## Subpath modules Two helper modules ship as separate subpath imports for tests that need HTTP-shaped fixtures. Both are cross-runtime (Node, Bun, Deno via `node:http`); browsers don't need them because running an HTTP server inside a webpage isn't a use case. ### tape-six/server — HTTP server harness ```js import {withServer, startServer, setupServer} from 'tape-six/server'; ``` #### `withServer(serverHandler, clientHandler, opts?)` — scoped resource (95% case) Spins up an `http.Server` with `serverHandler`, runs `clientHandler` with the bound base URL, tears down in `finally`. Cleanup runs whether `clientHandler` resolves, rejects, or throws synchronously. Returns whatever `clientHandler` returns. ```js test('GET / returns 200', t => withServer(handler, async base => { const res = await fetch(`${base}/`); t.equal(res.status, 200); })); ``` `serverHandler` is the per-request callback Node invokes on each incoming request. `clientHandler` is the per-scope test body, called once with `(base, lifecycle)` — naming reflects role on the wire (server side / client side), not which side is the SUT. Either side may be the code under test. `clientHandler` may make HTTP requests itself (`fetch(base)`-style), or set up a separate SUT that does (e.g., a spawned CLI given `${base}` as its endpoint env var), or do neither (multi-phase setup + assertions). #### `startServer(server, opts?)` — procedural primitive For multi-phase tests that span the lifecycle, or non-test code (e.g. `bin/tape6-server`) that wants long-term control. Accepts a fully-constructed `http.Server` (so callers can attach `'clientError'` handlers, configure TLS, etc., before listen). ```js const lc = await startServer(http.createServer(handler), {host, port}); // ... use lc.base ... await lc.close(); ``` Returns a lifecycle handle: - `server` — the underlying `http.Server`. - `base` — bound base URL, e.g. `"http://127.0.0.1:54321"`. - `port` — actual bound port (OS-assigned when `port: 0`). - `host` — bound host. - `close()` — idempotent. Calls `server.closeAllConnections()` (when available) so keep-alive sockets don't delay teardown. Races `'listening'` against `'error'`: port-busy / `EACCES` rejects with the original error rather than hanging. Existing helpers across the toolkit ecosystem don't do this and have hung CI machines as a result. #### `setupServer(serverHandler, opts?)` — hook helper Registers `beforeAll` (start) and `afterAll` (close) and returns a live-getter handle for suite-shared servers: ```js const server = setupServer(handler); test('first', async t => { const res = await fetch(`${server.base}/foo`); }); test('second', async t => { const res = await fetch(`${server.base}/bar`); }); ``` The returned object is `Object.freeze`d and uses property getters that read the live lifecycle. Don't destructure at module load (`const {base} = setupServer(...)`) — `base` is `undefined` until `beforeAll` runs. State reset stays user-side. For mock-server scenarios that need per-test state clearing, compose your own `beforeEach`: ```js const server = setupServer((req, res) => { recorded.push({method: req.method, url: req.url}); res.writeHead(204).end(); }); let recorded; beforeEach(() => { recorded = []; }); ``` `setupServer` owns the suite lifecycle; the caller owns suite state. #### Options ```ts interface ServerOptions { host?: string; // default '127.0.0.1' — explicit IPv4 avoids dual-stack ambiguity port?: number; // default 0 — OS-assigned } ``` ### tape-six/response — HTTP response helpers Reading helpers that work uniformly with both `Response` (fetch results) and Node `http.IncomingMessage`: ```js import {asText, asJson, asBytes, header, headers} from 'tape-six/response'; ``` - `asText(res)` → `Promise` — body as UTF-8. - `asJson(res)` → `Promise` — body parsed as JSON. - `asBytes(res)` → `Promise` — body as raw bytes. - `header(res, name)` → `string | null` — case-insensitive single-header read. Array-valued `IncomingMessage` headers (e.g. `set-cookie`) are joined with `, `. - `headers(res)` → `Record` — all headers as a plain object with lowercase keys. For status code, just use `res.status` — same on both `Response` and `IncomingMessage`. ## Configuring tests Configuration is read from `tape6.json` or the `"tape6"` section of `package.json`: ```json { "tape6": { "tests": ["/tests/test-*.*js"], "cli": ["/tests/test-*.cjs"], "browser": ["/tests/browser/test-*.html"], "importmap": { "imports": { "tape-six": "/node_modules/tape-six/index.js", "tape-six/": "/node_modules/tape-six/src/" } } } } ``` Environment-specific subsections: `cli` (CLI-only: Node, Bun, Deno), `node`, `deno`, `bun`, `browser`. ## Command-line utilities ### tape6 Runs test files in parallel using worker threads. ```bash tape6 [options...] [tests...] ``` - `--flags FLAGS`, `-f FLAGS` — output control flags (see below). - `--par N`, `-p N` — number of parallel workers (default: all CPU cores). - `--info` — print current configuration and exit without running tests. - `--self` — print the path to this script and exit. - `--help`, `-h` — show help message and exit. - `--version`, `-v` — show version and exit. - No arguments: runs tests from configuration. - Options accept `--flags FO` or `--flags=FO`. The `=` form does not support quoting. ### tape6-seq Sequential in-process runner. Same options as `tape6` but no threads. ### tape6-server Web server for browser testing: ```bash tape6-server [options...] ``` - `--trace` — enable request trace logging. - `--self` — print the path to this script and exit. - `--help`, `-h` — show help message and exit. - `--version`, `-v` — show version and exit. Configured via environment variables: `HOST` (default: localhost), `PORT` (default: 3000), `SERVER_ROOT` (default: cwd), `WEBAPP_PATH`. Navigate to `http://localhost:3000` for the web UI. ### Runtime-specific runners - `tape6-node` — Node.js runner. - `tape6-bun` — Bun runner. - `tape6-deno` — Deno runner. ## Supported flags Flags are a string of characters. Uppercase = enabled, lowercase = disabled. - `F` — Failures only: show only failed tests. - `T` — show Time for each test. - `B` — show Banner with summary. - `D` — show Data of failed tests. - `O` — fail Once: stop at first failure. - `N` — show assert Number. - `M` — Monochrome: no colors. - `C` — don't Capture console output. - `H` — Hide streams and console output. Usage: ```bash tape6 --flags FO TAPE6_FLAGS=FO node tests/test-example.js ``` Browser: `http://localhost:3000/?flags=FO` ## Environment variables - `TAPE6_FLAGS` — flags string. - `TAPE6_PAR` — number of parallel workers. - `TAPE6_TAP` — force TAP reporter (any non-empty value). - `TAPE6_JSONL` — force JSONL reporter (any non-empty value). - `TAPE6_MIN` — force minimal reporter (any non-empty value). - `TAPE6_TEST_FILE_NAME` — set by runners to identify the current test file. ## Browser testing 1. Start the server: `npm start` (runs `tape6-server --trace`). 2. Open `http://localhost:3000` for the web UI. 3. For automated headless testing use dedicated packages: - tape-six-puppeteer (https://www.npmjs.com/package/tape-six-puppeteer) - tape-six-playwright (https://www.npmjs.com/package/tape-six-playwright) ## Related packages - tape-six-proc (https://www.npmjs.com/package/tape-six-proc) — runs test files in separate processes instead of worker threads. - tape-six-puppeteer (https://www.npmjs.com/package/tape-six-puppeteer) — automates browser testing with Puppeteer. - tape-six-playwright (https://www.npmjs.com/package/tape-six-playwright) — automates browser testing with Playwright. ## 3rd-party libraries (bring-your-own) `tape-six` doesn't bundle assertion / mock / property-based libraries. It catches whatever the test throws — anything throwing a node-compatible `AssertionError` (with `name`, `operator`, `actual`, `expected`) is reported as a regular assertion; other errors are reported as `UNEXPECTED EXCEPTION` (test still fails). **Verified candidates** (full per-lib examples and browser importmap snippets in the wiki): - `node:assert` — built-in to Node/Bun/Deno, throws `AssertionError`. Cleanest path. - `chai` — `expect`/`should`/`assert` styles, throws `AssertionError`. Works in browsers via importmap entry. - `expect` (Jest's standalone) — throws plain `Error`, not `AssertionError`. Works but failures render as `UNEXPECTED EXCEPTION` with ANSI-colored messages. Wrap negative cases in `t.throws()`. - `node:test` mock — built-in spies/stubs/timers via `import {mock} from 'node:test'`. Doesn't throw; assert on `spy.mock.calls` with `t.*`. - `sinon` — same pattern as `node:test` mock. Assert on `spy.callCount` / `spy.firstCall.args`. - `fast-check` — property-based testing. Throws plain `Error` on counterexample (with seed + path for repro). Wrap negative cases in `t.throws()`. ```js import test from 'tape-six'; import assert from 'node:assert/strict'; test('with node:assert', t => { assert.equal(1 + 1, 2); assert.deepEqual({a: 1}, {a: 1}); }); ``` ```js import test from 'tape-six'; import {mock} from 'node:test'; test('with node:test mock', t => { const spy = mock.fn(); spy(1, 2); t.equal(spy.mock.calls.length, 1); t.deepEqual(spy.mock.calls[0].arguments, [1, 2]); }); ``` Wiki: [3rd-party assertion libraries](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-assertion-libraries), [mock libraries](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-mock-libraries), [property-based testing](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-property-based-testing). Assertions that throw `AssertionError` are automatically caught and reported.