bnl_test

The bnl_test module is a tiny test framework with a familiar API. It supports suites (describe), tests (test/it), hooks (before/after/beforeEach/afterEach), timeouts, skip/todo/only, and a programmatic run(). It also auto‑runs collected tests on process drain unless disabled.


API Surface

Defining

  • describe(name, [options], fn) — define a suite; can nest.
  • test(name, [options], fn) / it(...) — define a test.
  • Variants: test.skip(name, fn), test.todo(name), test.only(name, [options], fn).

Hooks

  • before(fn), after(fn) — run once per suite.
  • beforeEach(fn), afterEach(fn) — run around each test in suite nesting order.

Running

  • run([options]){ pass, fail, skip, todo, duration }.
  • Auto‑run: tests run automatically on beforeExit unless BNL_TEST_AUTORUN=0 env is set.

Test options

  • timeout (ms; default 2000) — per‑test timeout.
  • only (boolean) — focus this test.
  • skip, todo (boolean).
  • done (boolean) — allow callback‑style completion (alternative to returning a Promise).

Test context t

  • t.diagnostic(msg) — add a diagnostic line to the report.
  • t.cleanup(fn) — register cleanup (runs LIFO even on failures).
  • t.setTimeout(ms) — change current test timeout at runtime.
  • t.test(name, [options], fn) — schedule a subtest under the same parent suite.
  • t.skip() — skip the current test.

Behavior & Semantics

  • Only mode: if any test/suite is marked only, the runner executes only those tests (and their parents).
  • Skip vs Todo: skipped tests are reported as skipped; todo are placeholders (no function).
  • Timeouts: exceeding a test’s timeout fails it with TestTimeoutError. You can adjust per test using options or t.setTimeout(ms).
  • Execution order: depth‑first, serial; suite before runs once, beforeEach/afterEach wrap each test, then after runs once.
  • Errors: any thrown error (or rejected promise) fails the test; cleanups still run.

Examples (English only)

const { test, it, describe, before, after, beforeEach, afterEach, run } = require("bnl_test");

describe("math", () => {
  let acc = 0;

  before(() => { acc = 0; });
  beforeEach(() => { acc += 1; });
  afterEach(() => { acc = 0; });

  it("adds", async (t) => {
    t.diagnostic("starting");
    const sum = 1 + 2;
    if (sum !== 3) throw new Error("bad math");
    t.cleanup(() => console.log("cleanup called"));
  });

  test.skip("multiplies", () => {});
  test.todo("divides later");
});

// Focused test (only)
test.only("focused", async () => {
  await new Promise(r => setTimeout(r, 10));
});

// Programmatic run (optional; auto-run happens by default)
(async () => {
  const summary = await run();
  console.log("summary:", summary);
})();
// Done-style callback test (avoids returning a Promise)
test("with done", { done: true }, (t, done) => {
  setTimeout(() => done(), 5);
});
// Subtests from within a test
test("parent", async (t) => {
  t.test("child 1", () => {});
  t.test("child 2", { timeout: 100 }, async () => {});
});

Notes

  • Autorun can be disabled by setting environment variable BNL_TEST_AUTORUN=0.
  • Exit code: failures set process.exitCode = 1 when autorun triggers.
  • Color reporter: prints ✓/✗ with diagnostics; diags are added via t.diagnostic.
  • Keep tests small and deterministic since execution is serial.