esdev test

Shell
esdev test                    # every *.test.{js,mjs,ts,tsx,jsx}
esdev test math               # ...whose path contains "math"
esdev test --file=x.test.ts   # exactly one file

Exits non-zero if any file fails.

Write one

The API comes from runtime:test, like everything else on this runtime.

TypeScript
import { test, assertEquals, assertRejects } from "runtime:test";

test("adds", () => {
  assertEquals(1 + 1, 2);
});

test("rejects", async () => {
  await assertRejects(() => fetch("http://127.0.0.1:1/"));
});

Because it is an import, a helper module beside your test file can use the assertions too:

TypeScript
// helpers.ts
import { assertEquals } from "runtime:test";
export const assertSorted = (xs: number[]) => assertEquals(xs, [...xs].sort());
test(name, fn)fn may be async; failures are collected
assert(cond, msg?)
assertEquals(actual, expected, msg?)Structural
assertThrows(fn, expected?, msg?)
assertRejects(fn, expected?, msg?)

The same vocabulary the runtime's own conformance suite uses: reading its tests and writing your own should not mean learning two.

assertEquals walks the values: BigInt and NaN compare, typed arrays and ArrayBuffer compare as bytes, Map and Set by contents, objects by their key set rather than key order, and a cyclic structure terminates.

expected is what the error must be — omit it to accept any throw.

"TypeError"the error's name, or a substring of its message
/too long/tested against the message
HttpErroran instanceof check

One process per file

A test suite is where isolation matters most. A file that wedges, exhausts its heap or calls exit() must not decide the fate of the others, and a global left behind by one must not be visible to the next. Each file gets its own process — the prelude snapshot makes that cheap.

--file=<path> is what a child is invoked with, and is equally a supported way to run one file by hand.

The file is the entry, unaltered

It keeps its own path, its module resolution, its relative imports and its TypeScript — and nothing is added to it. What runs is byte for byte the file on disk, so a failing assertion names the line you wrote because it is the line you wrote.

That was not always true. The API used to be five globals prepended to the file as a single physical line, with an epilogue appended to await and report, and keeping your line 1 as line 1 was a constraint the harness had to be written around. Moving the API into a module and the tally into the host removed both.

Since a test file is now an ordinary module, running one directly works and reports the same way:

Shell
esdev app.test.ts

A test that never finishes

It is reported as a failure, not a hang:

TEXT
  FAIL never finishes
    the test never finished — it is waiting on something that never happened
  3 passed, 1 failed

The runner knows a case started and never settled. It used to await every pending promise, so a test waiting on something that never happened hung the file forever.

Not yet

Parallelism across files, describe nesting, per-file timeouts, and a machine-readable reporter.

Last updated on
Edit this page