Skip to content
Standard library

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.

ts
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 from velt 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, assertEq or assertThrows throws an AssertionError, 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_x with the error), without the running lines 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, it and 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 the describe callbacks, 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. --filter is applied to the full names here.
  • A describe callback that throws (or is async) is reported as a failed test named as its group, and the group's tests don't run. An afterAll that throws is printed where it ran and fails the run. A test's name is escaped on its report line (\n for 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.

ts
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>.

Edit this page on GitHub