velt/test
import { describe, it, expect } from "velt/test". The test runner, written in Velt, with
Vitest's API; import ... from "vitest" imports this module too. velt test
(Testing) compiles the test files into a program whose generated main
registers their tests with this module and hands it control. Everything after that happens here.
import { describe, it, expect, beforeEach } from "velt/test";
describe("stack", () => {
let items: i64[] = [];
beforeEach(() => {
items = [1, 2];
});
it("pushes", () => {
items.push(3);
expect(items).toHaveLength(3);
expect(items).toEqual([1, 2, 3]);
});
it("pops", () => {
expect(items.pop()).toBe(2);
expect(items).not.toContain(2);
});
});How a run works
- As the runner (started by
velt test), the program reads the run's plan fromvelt test, starts a process per test file (from the program that holds the file; several at once,--jobs), prints each file's report in file order and the summary, and exits with 1 if a test failed. - For one file (in a process the runner started), it runs the file's tests one after
another. A test fails when it throws: a failed
assert,assertEqorassertThrowsthrows anAssertionError, and any other error it doesn't catch counts the same. The error is reported with where it was thrown, as an uncaught error would be (Error: boom at tests/a.test.vlt:6:3), after the last line of a message of several lines (as Vitest shows the location after the message), and the next test runs. A test that ends the process (a panic,process.exit) is reported with its exit code and stderr, and the runner starts the file again from the next test. After the last test the process exits, also when a failed test left a timer or a server running. A process that ends before its first test (in a module's top-level code) fails all of the file's tests. - By hand (a test program from
velt test --no-run, run directly or under a debugger), it runs every test of its files in this process and prints each test's result (ok test_x,FAILED test_xwith the error), without therunninglines and the summary.
The runner is a program of its own, without test files, so no test file's code runs in it.
describe, it and expect
describe, it, test, beforeAll, afterAll, beforeEach, afterEach, expect and
Expect are Vitest's API. How they run:
- Called at a test file's top level,
describe,itand the hooks only queue what they register. A test program may hold several test files, and every module it imports is evaluated when it starts, so this is all their top level does in each process. - In the process for one file, its tests are collected: its
test_*functions, then what its queued calls register, running thedescribecallbacks, whose calls register into their group at once. The process lists the tests to the runner (their full names,group > test) and runs them from the one the runner asks for, with the hooks of their groups.--filteris applied to the full names here. - A
describecallback that throws (or isasync) is reported as a failed test named as its group, and the group's tests don't run. AnafterAllthat throws is printed where it ran and fails the run. A test's name is escaped on its report line (\nfor a line break).
The matchers: toBe, toEqual, toBeNull, toBeUndefined, toBeDefined, toBeTruthy,
toBeFalsy, toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan,
toBeLessThanOrEqual, toContain, toHaveLength, toMatch and toThrow, each with .not;
.resolves and .rejects for promises.
toEqual compares as deepEqual does, but a class instance that isn't the
same object compares by its fields, and numbers with Object.is. The fields are those of the
object's own class, whatever type holds it: two Shapes (an interface) or two Animals (a base
class) are equal when they are objects of one class with equal fields, a subclass's fields
included, so expect(shapes).toEqual([...]) works on a Shape[]. Objects of two different
classes are unequal; Vitest's toEqual compares them by their fields' names too (its
toStrictEqual doesn't), so there a Cat with the fields of an Animal equals the Animal.
Cycles go by Vitest's rule: an object met again with the same partner while being compared is
equal (a parent pointer, rings of one length), with another partner different (rings of two
lengths).
expect(actual) returns an Expect<T>: actual, whether .not was used (negated) and the
matchers, which are methods (the generic ones) and extend blocks (those for arrays, strings,
Comparable types, functions and promises). check(pass, message, notMessage?) throws the
AssertionError of a failed matcher; custom matchers call it too. .resolves and .rejects
return a Resolves<V, E> or Rejects<V, E>, whose matchers are async; their settled() is
the Expect of the value or the error, for a custom matcher that awaits.
import { expect, test, Expect } from "velt/test";
extend Expect<string> {
toBeShout() {
this.check(this.actual == this.actual.toUpperCase(), `expected "${this.actual}" to be upper case`);
}
}
test("shouts", () => {
expect("HEY").toBeShout();
expect("hey").not.toBeShout();
});toMatch and toThrow take a string or a RegExp. expect doesn't import
velt/text: a test file compiles the regex engine only when it has a regex (a
literal such as /^h/, or RegExp named), as any program does.
velt/test/http adds toHaveStatus and toHaveHeader to
Expect<Response>.