js/test/test
js/test/test.ts
fino:test — TAP-13 test framework with nesting and BDD-style describe/it.
Two equivalent but non-mixable patterns:
Pattern 1: suite + test
import { test, suite } from './test.ts';
test('standalone', (t) => { t.ok(true); });
suite('math', () => {
test('adds', (t) => { t.equal(1 + 1, 2); });
test('subs', (t) => { t.equal(2 - 1, 1); });
});
Pattern 2: describe + it + lifecycle hooks
import { describe, it } from './test.ts';
describe('math', () => {
before(async () => { ... }); // once, before first it
beforeEach(async () => { ... }); // before each it
afterEach(async () => { ... }); // after each it (always runs)
after(async () => { ... }); // once, after last it (always runs)
it('adds', (t) => { t.equal(1 + 1, 2); });
it('subs', (t) => { t.equal(2 - 1, 1); });
});
Mixing is forbidden: it() inside suite(), test() inside describe(),
suite() inside describe(), or describe() inside suite() all throw.
it() and hook functions throw if used at the top level.
TAP-13 output and captured diagnostics
Nested groups produce standard TAP subtests (indented 4 spaces per level):
TAP version 13 1..2 ok 1 - standalone # Subtest: math 1..2 ok 1 - adds ok 2 - subs ok 2 - math # tests 2 # pass 2
Console output from tests is captured by default so passing tests keep TAP
output clean. Failing tests print a final # Failure details section with
their captured stdout, stderr, and thrown errors. The CLI exposes this as
fino test --show-output=failures|always|never: failures is the default,
always streams output live for debugging, and never suppresses captured
output even when tests fail.
The release contract is intentionally smaller than Node's node:test API:
TAP output, name filters, skip reasons, per-test metadata, duration
annotations, lifecycle hooks, and captured output are supported. only,
todo, per-test timeouts, concurrency controls, subtest creation from an
assertion object, and pluggable reporters are not part of this module.
Internal representation
Both APIs share a tree of nodes:
Leaf: { name, fn, children: null, skip: string|null } Group: { name, kind: 'suite'|'describe', children: [, before, beforeEach, after, afterEach, skip: string|null }
_current points at the group being registered into (null = top level).
The unified runner _runEntries(entries, depth, parentNode) recurses the tree,
applying hooks from parentNode to each leaf inside a describe group.
Types
type TestMetadataValue = string | number | boolean | bigint | null | undefined
Primitive value accepted by TestContext#meta().
type TestMetadata = Record<string, TestMetadataValue>
Metadata object accepted by TestContext#meta().
Keys must match [A-Za-z_][A-Za-z0-9_.:-]*. Repeated calls merge keys, and
later values overwrite earlier values.
type SkipOption = boolean | string
Value accepted by the skip registration option.
true skips without a reason, and a string is printed as the TAP skip
reason.
type RegisterOptions = {
skip?: SkipOption;
}
Options accepted by test(), suite(), describe(), and it().
type TestFn = (t: TestContext) => void | Promise<void>
Callback used by test() and it().
The assertion helper collects failures for TAP output. Returning a promise lets the runner await async test work.
type GroupFn = () => void
Registration callback used by suite() and describe().
type HookFn = () => void | Promise<void>
Lifecycle hook callback used by before(), after(), beforeEach(), and
afterEach().
Interfaces
interface RunOptions {
Options passed to run() when executing registered tests manually.
filter keeps only matching describe() paths and their ancestors. The CLI
passes this from fino test --filter.
showOutput controls test console output. failures captures output and
prints it only in final failure diagnostics, always writes output as tests
run, and never keeps captured output hidden. The CLI passes this from
fino test --show-output.
durations appends runner-owned duration=<time> metadata to every TAP
result line. The CLI passes this from fino test --durations.
Properties
filter?: string
showOutput?: ShowOutputMode
durations?: boolean
Classes
class TestContext extends Assert {
Assertion context passed to test() and it() callbacks.
TestContext extends Assert, so existing assertion calls such as
t.equal(actual, expected) continue to work. The additional meta() method
attaches primitive key/value metadata to the leaf test's TAP result line.
Repeated calls merge keys and later values overwrite earlier ones.
Metadata keys must match [A-Za-z_][A-Za-z0-9_.:-]*. Values may be strings,
numbers, booleans, bigints, null, or undefined.
import { test } from 'fino:test/test';
test('parses empty input', (t) => {
t.meta({ case: 'empty', rows: 0 });
t.equal(parse(''), []);
});
Methods
meta(values: TestMetadata): void
Attach primitive metadata to this test's final TAP result line.
Later calls merge with previous metadata and overwrite duplicate keys.
Invalid keys or unsupported value types throw TypeError, which fails the
current test like any other thrown error.
Functions
function test(name: string, optsOrFn: TestFn | RegisterOptions, maybeFn?: TestFn): void
Register a test case. Can be top-level or inside suite().
Throws inside describe().
The callback receives an Assert instance that collects all assertion
failures before the runner reports the test result. Pass { skip: true } or
{ skip: 'reason' } as the middle argument to mark the test skipped.
import { test } from 'fino:test/test';
test('adds numbers', (t) => {
t.equal(1 + 1, 2);
});
function suite(name: string, optsOrFn: GroupFn | RegisterOptions, maybeFn?: GroupFn): void
Register a group of tests. Can be nested inside other suite() calls.
Throws inside describe().
Suites are grouping-only; they do not support lifecycle hooks. Use
describe() when tests need before, after, beforeEach, or
afterEach.
import { suite, test } from 'fino:test/test';
suite('math', () => {
test('adds', (t) => t.equal(1 + 1, 2));
});
function describe(name: string, optsOrFn: GroupFn | RegisterOptions, maybeFn?: GroupFn): void
Register a BDD-style test group with optional lifecycle hooks.
Can be nested inside other describe() calls.
Throws inside suite().
The registration callback runs immediately and should only register tests and
hooks. Runtime work belongs inside it() callbacks or lifecycle hooks.
import { describe, it } from 'fino:test/test';
describe('api', () => {
it('responds', (t) => t.ok(true));
});
function it(name: string, optsOrFn: TestFn | RegisterOptions, maybeFn?: TestFn): void
Register a test case inside describe(). Throws outside describe().
The callback may be synchronous or async and receives the same assertion
helper used by test(). A parent describe({ skip }) propagates to all
child it() calls.
import { describe, it } from 'fino:test/test';
describe('user lookup', () => {
it('returns a user', async (t) => {
t.ok(await Promise.resolve({ id: 1 }));
});
});
function before(fn: HookFn): void
Run fn once before the first it in this describe block.
Throws outside describe().
A failing before() marks each entry in the group failed. Use it for shared
setup that every test in the block requires.
import { before, describe, it } from 'fino:test/test';
describe('database', () => {
before(async () => {
// connect
});
it('queries', (t) => t.ok(true));
});
function after(fn: HookFn): void
Run fn once after the last it in this describe block.
Always runs even if tests fail. Throws outside describe().
Errors thrown by after() are reported as failures with captured output,
while still running after earlier test or hook failures.
import { after, describe, it } from 'fino:test/test';
describe('server', () => {
after(async () => {
// close server
});
it('starts', (t) => t.ok(true));
});
function beforeEach(fn: HookFn): void
Run fn before each it in this describe block.
Throws outside describe().
If beforeEach() fails, the test body is skipped and the entry is reported
failed. Use it for per-test state that must be fresh.
import { beforeEach, describe, it } from 'fino:test/test';
describe('counter', () => {
let value = 0;
beforeEach(() => { value = 0; });
it('increments', (t) => t.equal(++value, 1));
});
function afterEach(fn: HookFn): void
Run fn after each it in this describe block.
Always runs even if the test fails. Throws outside describe().
Failures from afterEach() are collected with assertion failures from the
same test. Use it for cleanup that should be visible when it fails.
import { afterEach, describe, it } from 'fino:test/test';
describe('temp files', () => {
afterEach(async () => {
// remove temp files
});
it('writes', (t) => t.ok(true));
});
async function run(options: RunOptions = {}): Promise<void>
Run all registered tests and print TAP-13 output.
Called automatically by the fino test command. User test files
only need to call test() / suite() / describe() — never run().
import { run, test } from 'fino:test/test';
test('manual runner', (t) => t.ok(true));
await run({ filter: 'manual' });