Discovering the Bnlang Test Framework

Bnlang ships with a tiny test framework in the standard library — test. It gives you test(name, fn) to register tests, a handful of assertion helpers, and run() to execute everything and exit non-zero on failure.

There's no separate test discovery binary. A test file is just a regular Bnlang script that imports test, registers cases, and calls t.run().


A Minimal Test File

// file: tests/math_spec.bnl
import "test" as t;

t.test("addition", function () {
    t.equal(2 + 2, 4);
});

t.test("upper case", function () {
    t.equal("foo".to_upper(), "FOO");
});

t.run();

Available Assertions

The framework keeps its surface tiny. Every assertion throws on failure; run() catches and reports.

  • t.equal(actual, expected) — value compare.
  • t.not_equal(actual, expected) — opposite of equal.
  • t.is_true(v) / t.is_false(v) — boolean checks.
  • t.is_null(v) / t.not_null(v) — null checks.
  • t.contains(haystack, needle) — substring (for strings) or membership (for lists).
  • t.throws(fn) — assert that fn() throws.
  • t.throws_with(fn, substring) — like throws, but also checks the message.

Running a Test File

Just run the file with bnl. t.run() prints a summary to stdout and exits the process with code 1 on any failure, so CI picks it up automatically.

bnl tests/math_spec.bnl

Best Practices

  • Keep each test() focused on a single behavior — the name should describe the assertion.
  • Group related test files under a tests/ directory. There's no magic discovery, but it makes shell globs (bnl tests/*.bnl) straightforward.
  • Use t.throws_with to assert on error messages — it catches regressions where the right exception fires for the wrong reason.