# ubq A test tool for TypeScript and JavaScript, made for coding agents and the people who work with them. The package is `ubqtest`. The command is `ubq`. It runs on Node 24 or later, Bun, and Deno, with no build step and no configuration. > **Status:** not released yet. CI passes on Linux, macOS, and > Windows, with Node, Bun, and Deno. ## Why a different test tool An agent reads test output, decides, and runs a command again, many times in one task. Most test tools print for a person at a terminal: progress, colors, and a long report. ubq is made for that loop. - **Short output.** A pass is one line. A failure is one block with the place, the source line, the difference, and the command that runs that one test again. - **Rules that catch a test that passes for the wrong reason.** A test with no assertion fails. A promise that nothing waits for fails. Work that goes on after the test ended fails. A test that leaves `process.env` changed fails. - **One small API.** `test`, `describe`, `assert`, and a few helpers. No globals, no hooks, no `.only`. A form from a different tool, such as `assert.ok` or `test.only`, is a type error whose message gives the form to use. - **Coverage for each test, always on.** ubq knows which tests ran each line. It uses that to run only the tests that a change can affect, and to answer questions. - **A store of past runs.** Failures, history, and coverage are in a local SQLite file. An agent asks a question with a command and does not run the tests again to see the output. - **The guide is in the package.** `ubq guide` prints the API and the rules for the installed version. ## Start ``` npm install --save-dev ubqtest ``` ```ts // test/parse.test.ts import { assert, describe, test } from 'ubqtest' import { parse } from '../src/parse.ts' describe('parse', () => { test('circle', () => { const shape = parse('c') assert(shape) // narrows away undefined assert.equal(shape.kind, 'circle') }) test('unknown input', () => { assert.throws(() => parse('?'), /unknown shape/) }) }) ``` ``` $ npx ubq pass: 0 failed, 2 passed (run 1) ``` A failure: ``` $ npx ubq FAIL test/parse.test.ts > parse > circle expected values to be equal at test/parse.test.ts:9:12 > assert.equal(shape.kind, 'circle') actual: 'square' expected: 'circle' rerun: ubq test/parse.test.ts:6 fail: 1 failed, 1 passed (run 2) ``` A test file is also a plain program: `node test/parse.test.ts`, `bun test/parse.test.ts`, `deno run -A test/parse.test.ts`. ## Commands | Command | Does | | --- | --- | | `ubq` | Runs all test files (`*.test.ts`, `*.test.js`, and so on), and the type checker of the project. | | `ubq `, `ubq :`, `ubq ''` | Runs one file, one test, or one group. | | `ubq --changed` | Runs the tests that ran code that changed after their last run. | | `ubq --failed` | Runs the tests that failed in their last run. | | `ubq --repeat ...` | Runs tests n times and counts the failures of each, to find a flaky test. | | `ubq --baseline ...` | Shows only the failures that the commit does not have. | | `ubq failures`, `ubq show`, `ubq history`, `ubq diff`, `ubq slow` | Questions about stored runs. | | `ubq uncovered`, `ubq covers :` | Code of the program that no test ran (exit code 1 if there is some). The tests that ran a line. | | `ubq uncovered --diff` | Code that you changed and that no test runs. | | `ubq merge ...` | Adds the coverage of other stores, such as those of CI jobs on other runtimes and systems. | | `ubq mutate `, `ubq mutate --diff` | A mutation check: small changes to the source, one at a time, and the list of changes that no test fails for. | | `ubq convert ` | Changes the tests of tap, `node:test`, vitest, jest, or mocha to ubq. | | `ubq browser install` | Downloads the browser for tests that use `'ubqtest/browser'`. | | `ubq doctor` | Checks the setup of the project. | | `ubq guide`, `ubq guide convert` | The guides, for the installed version. | | `ubq server` | Optional. Keeps test processes ready, which makes runs faster. | Exit codes: 0 pass, 1 failure, 2 no tests ran, 4 wrong command or configuration. ## What a run does - Each test file runs in its own process. Files run in parallel, in a random order. A run with a failure prints the seed. - The type checker of the project (`tsc --noEmit`) runs at the same time, if the project has `tsconfig.json`. A type error fails the run. A run of a selection fails only for the type errors of the files that it used. - A test that goes over its time limit fails, and the report names what it left open and the line that opened it. - A test file with a syntax error or a missing import is one `ERROR` block with the line. ## For coding agents Tell the agent that the project uses ubq, and where the guide is. For example, in `AGENTS.md` or `CLAUDE.md`: ```md Tests use ubq (the ubqtest package). Before you write or run tests, read `npx ubq guide`. Run `npx ubq --changed` after a change, then `npx ubq uncovered --diff`. ``` `npx ubq skill` writes a skill (`use-ubq/SKILL.md`) for coding agents. It writes to each skill folder that the project has (`.claude/skills`, `.cursor/skills`, `.agents/skills`, and others of that form), and to the folder of the agent that runs the command: Claude Code, Cursor, Gemini, Copilot, or Codex. With none of these, it writes to `.claude/skills`. To select the place, name the agent or a folder: `npx ubq skill cursor`, `npx ubq skill .myagent/skills`. The skill tells the agent to read the guides of the installed package. The guides are also files of the package: `GUIDE.md` and `CONVERT.md`. ## Move a project to ubq ``` npx ubq guide convert # the steps npx ubq convert test # the certain changes, and a TODO for each of the others ``` `ubq convert` knows tap, `node:test` with `node:assert`, vitest, jest, and mocha with `node:assert`. It changes imports, test definitions, and assertions. Where a person or an agent must decide (hooks, subtests, module mocks), it writes a `TODO(ubq)` comment with the instruction. ## Requirements and limits - Node 24 or later, or Bun, or Deno. - On Node, TypeScript runs with its types removed and no other change. Source with `enum`, `namespace`, or parameter properties does not load on Node. Bun runs it. - `.tsx` and `.jsx` files run on Bun and Deno, with coverage. - Tests of one file run one at a time. - A test that starts a command or a server as a child process gets the coverage of that process, on Node and Bun. - Tests of front-end code run in a real browser, with Playwright and Chromium: `import { browser } from 'ubqtest/browser'`. The project installs `playwright-core`. There is no DOM in the test process. - Tests of a Cloudflare Worker run in workerd, with the bindings of the wrangler config: the `workers` key, and `'ubqtest/workers'`. The project installs `wrangler`. - Configuration is the `ubq` key of `package.json`: `include`, `exclude`, `timeout`, `browser`, `workers`, and two switches. Most projects need none. See [docs/08-config.md](config.md). ## More documents - [GUIDE.md](guide.md): the API, the rules, and the commands. - [CONVERT.md](convert.md): how to move a project to ubq. - [docs/api.md](api.md): each export of the package, made from the type declarations. - [docs/](docs.md): the design, the decisions and their reasons, measurements, and the status of the work. ## Development ``` npm install npm test # node src/cli.ts: the tool runs its own tests npm run typecheck npm run build # the package, in dist/ npm run docs # docs/api.md npm run site # the web site, in _site/ ``` The tests need `bun` and `deno` on the path. A test skips itself if its runtime is not installed. ## License [Blue Oak Model License 1.0.0](license.md) --- # ubq guide How to write and run tests with `ubqtest`. This text comes from the installed package, so it is correct for the installed version. `ubq guide` prints it. `ubq guide convert` tells how to move a project from a different test tool. `ubq doctor` checks the setup of a project: it prints one `FIX` line for each problem, and its exit code is 1 if there is a problem. ## A test file ```ts // test/parse.test.ts import { assert, describe, test } from 'ubqtest' import { parse } from '../src/parse.ts' describe('parse', () => { test('circle', () => { const shape = parse('c') assert(shape) // narrows away undefined assert.equal(shape.kind, 'circle') }) test('unknown input', () => { assert.throws(() => parse('?'), /unknown shape/) }) for (const [input, kind] of [['c', 'circle'], ['s', 'square']] as const) test(`kind of ${input}`, () => { assert.match(parse(input), { kind }) }) }) ``` - The name of a test file ends in `.test.ts` (or `.test.js`, `.test.mts`, `.test.tsx`, and so on). The file can be in any folder of the project. - Import each function from `'ubqtest'`. There are no globals. - Import source files with their extension: `'../src/parse.ts'`. Under `ubq`, `'../src/parse.js'` also finds `parse.ts`. - A test file needs no build step and no configuration. ## Run ``` ubq all test files of the project ubq test/parse.test.ts one file (or a folder) ubq test/parse.test.ts:9 the test at that line ubq '' one test, or the tests of a group ubq --failed the tests that failed in their last run ubq --changed the tests that ran code that changed node test/parse.test.ts a test file is a plain program (also: deno run -A, bun) ``` Each run also runs the type checker, if the project has `tsconfig.json` and the `typescript` package. Each type error is a `TYPE` block, and the run fails. The tests run in any case. - A folder with its own `tsconfig.json` is checked too, if a test file of the run is in it. - A run of all tests (`ubq`) reports each type error. A run of a selection (`ubq `, `ubq --changed`) reports the type errors of the files that it used: its test files, and the files that their tests ran. It prints the count of the others in one line, and does not fail for them. Use `npx ubq` if `ubq` is not on the path. On Bun: `bun node_modules/.bin/ubq`. The last line of the output is the summary: `pass: 0 failed, 12 passed (run 7)` or `fail: 2 failed, 10 passed (run 8)`. Passed tests print nothing. Exit codes: 0 pass, 1 failure, 2 no tests ran, 4 wrong command or configuration. A failure block has the test ID, the message, the location, the source line, the difference, and the command that runs the test again: ``` FAIL test/parse.test.ts > parse > circle expected values to be equal at test/parse.test.ts:9:12 > assert.equal(shape.kind, 'circle') actual: 'square' expected: 'circle' rerun: ubq test/parse.test.ts:6 ``` ## Rules that fail a test - A test with zero assertions fails. There is no opt-out. - `assert.rejects` or `assert.eventually` with no `await` fails. - An assertion, a timer callback, or a promise of a test that runs after the test ended fails that test. - A test that changes `process.env` or the working directory and does not put the change back fails. Use `env()` and `cleanup()`. - A test that takes more than its time limit fails (default 5000 ms). The message lists what the test left open. Then ubq runs that test one more time, and a `HANG` block names the line that opened each thing: `Timeout opened at test/a.test.ts:18:32`. - Two tests with the same name in one group are an error. - A call to `process.exit` in a test file is an error. Do not weaken a test to get a pass. Each of these rules catches a test that would pass for the wrong reason. ## API [Complete API reference](/api) ```ts test(name, fn) test(name, { skip?: string | false, timeout?: number }, fn) describe(name, fn) // a name prefix for its tests. fn is sync. describe(name, { skip?, timeout? }, fn) assert(value, message?) // truthy. Narrows the type. assert.equal(actual, expected) // Object.is assert.notEqual(actual, expected) assert.deepEqual(actual, expected) // strict: same structure, values, prototypes assert.match(actual, pattern) // RegExp for a string, or a partial object assert.throws(fn, expected) // expected is required. Returns the error. await assert.rejects(promise, expected) // resolves to the error await assert.eventually(fn, pattern) // as match. Calls fn again until it matches, 2000 ms at most. assert.snapshot(value) // compares with saved text assert.fail(message) isDeepEqual(a, b) // true or false, by the rules of deepEqual. Not an assertion. spy(impl?) // records calls in spy.calls: { args, result, error } spyOn(object, key, impl?) // replaces a method until the test ends. A second spyOn on the key replaces the first. await mockImport(() => import('../src/a.ts'), [ [() => import('../src/dep.ts'), { fn: replacement }], ]) // a new instance of a.ts with a mocked dep.ts testdir({ 'a.txt': 'hi', sub: { 'b.json': '{}' } }) // a new folder, returns its path env({ NAME: 'value', OTHER: undefined }) // set or delete, until the test ends cleanup(() => server.close()) // runs when the test ends const clock = fakeClock({ now: 0 }) // await clock.tick(ms), await clock.runAll() ``` - `expected` of `throws` and `rejects` is a `RegExp` for the message, an error class, or a partial object. - An object pattern of `match` permits extra keys in the value. An array pattern must have the same length. - The expected value of `equal` and `deepEqual` must have the type of the actual value. If the type checker refuses a comparison, make the value exact (`value!`, or narrow it with `assert(value)`). To compare two different types on purpose: `assert.deepEqual(a, b)`. - The test function gets `{ signal }`. The signal aborts at the time limit. Pass it to `fetch` and to child processes. - `skip` is the reason, as text: `{ skip: 'needs a GPU' }`. A condition: `{ skip: process.platform === 'win32' && 'needs fork' }`. - Tests of a file run one at a time, in the order of the file. - `fakeClock` replaces the global timers and `Date`. On Node, it also replaces a name that a file imported from `node:timers` or `node:timers/promises`. On Bun and Deno, such a name keeps the real timer: use the global `setTimeout` there. Test files run in parallel, each in its own process, in a random order. A run with a failure prints the seed. `ubq --seed ` repeats the order. - For shared setup, write a function and call it in each test. `testdir`, `env`, `cleanup`, and `spyOn` at the top level of a file (before the first `test`) last until the file ends. - A failed test keeps its `testdir` folder, and the failure block prints the path. - The mock of `mockImport` must have the types of the real exports. It exists only in the returned module instance. The two loaders must have the form `() => import('...')`. A mock with a spy: ```ts test('checkout charges the card', async () => { const charge = spy(async (card: string, cents: number) => ({ id: 'r1', cents })) const { checkout } = await mockImport(() => import('../src/cart.ts'), [ [() => import('../src/payment.ts'), { charge }], ]) await checkout(items, 'card-1') assert.deepEqual(charge.calls[0]?.args, ['card-1', 700]) }) ``` - Snapshots are in `ubq-snapshots/`. Commit them. A value with no saved text fails. Only `ubq --update ` writes them. Read the new text before you commit it. ## Names that do not exist | Do not use | Use | | --- | --- | | `it` | `test` | | `expect(x).toBe(y)`, `toEqual` | `assert.equal(x, y)`, `assert.deepEqual(x, y)` | | `assert.ok`, `strictEqual`, `deepStrictEqual` | `assert(value)`, `assert.equal`, `assert.deepEqual` | | `beforeEach`, `afterEach`, `beforeAll`, `afterAll` | a setup function, `cleanup`, top-level code | | `test.only`, `describe.only` | `ubq :` or `ubq ''` | | `test.skip`, `test.todo`, `skipIf` | `test(name, { skip: 'reason' }, fn)` | | `test.each` | a `for` loop around `test()` | | `retry` | none. `ubq --repeat ` counts the failures of a flaky test. | | `vi.mock`, `jest.mock` | `mockImport` | | `vi.fn`, `jest.fn` | `spy` | | `vi.useFakeTimers` | `fakeClock` | | `toMatchSnapshot` | `assert.snapshot(value)` | | `assert.doesNotThrow` | call the function, then assert on its result | | a subtest in a test, `t.plan`, `t.end` | `describe`. A test ends when its function ends. | ## Questions about stored runs Each run is stored in `.ubq/runs.db` (git ignores `.ubq/`). These commands read the store and run no test: ``` ubq failures [run] the failures of a run again (default: the last run) ubq show '' the last result of a test, with its console output ubq history '' the result of a test in each stored run. Says if the test is flaky. ubq diff [run] [run] the tests whose status differs in two runs ubq slow [run] the slowest tests ubq uncovered [file] code that no test ran, in each file that a test loaded. Not test files, and not test helpers. Exit code 1 if it lists code. ubq uncovered --diff code that no test ran, in the lines that you changed (from the last git commit) ubq merge ... adds the coverage of other stores: a project folder, a .ubq folder, or a runs.db file ubq covers : the tests that ran a line ubq sql '' a read-only SQL query, as JSON lines ``` Coverage is always on, for each test. Before a change to a line, `ubq covers :` names the tests that protect it. After a change to the source, do these in order: ``` ubq --changed runs only the tests that ran the code that changed ubq uncovered --diff lists the code that you changed and that no test runs: write a test for it ubq mutate --diff a mutation check of the lines that you changed (see below) ``` `--diff` is the difference from the last git commit, with files that git does not have yet. `--diff=main` is the difference from a different commit. A mutation check tells if the tests of a file are strong: ``` ubq mutate src/cart.ts makes small changes to the file, one at a time, and runs the tests of the changed code ubq mutate src/cart.ts:20 only the changes on that line ubq mutate --diff only the lines that you changed ``` It changes an operator, a number, or a boolean (`-` to `+`, `<` to `<=`, `0` to `1`), in the test processes only. The file does not change. Each `SURVIVED` block is a change that no test failed for: add a test for that case. Some changes cannot make a test fail, because the changed code does the same thing. Do not change correct code for them. Run the tests before the check (`ubq`). More run commands: ``` ubq --repeat run the tests n times, count the failures of each ubq --baseline show only the failures that the git commit does not have ``` Console output of a test is captured. It shows only in the failure block of its test, and in `ubq show`. ## Tests in a browser For code that runs in a browser. The driver is Playwright, with Chromium. Set up one time: ``` npm install --save-dev playwright-core npx ubq browser install downloads the browser, if it is not there ``` ```ts import { assert, test } from 'ubqtest' import { browser } from 'ubqtest/browser' test('the button counts', async () => { const page = await browser() // a Page of Playwright, in a new context await page.goto('/test/pages/count.html') await page.click('#add') await assert.eventually(() => page.textContent('#total'), '1') }) ``` - `browser()` gives a `Page` of Playwright. Use the Playwright API for all work with the page: `goto`, `click`, `fill`, `locator`, `getByRole`, `evaluate`. There is no `expect`: read a value from the page and use `assert`. - A page changes with time. For a value that is not there at once, use `await assert.eventually(() => value, pattern)`. Do not use a fixed wait. - The page closes when the test ends. An error that the page does not catch fails the test. - An action waits 2000 ms for its element. Then it fails, and the message names the element. - The first `browser()` call of a file starts the browser. On a slow machine that takes seconds: give such a test a longer limit, `{ timeout: 30_000 }`. - `browser(options)` takes the options of `browser.newContext` of Playwright, such as `viewport` and `locale`. Coverage: the code of the project that a page runs is coverage of the test, as the code that the test process runs. So `ubq --changed`, `ubq uncovered`, and `ubq covers` work for it. This needs a script whose URL path is the path of a project file. The script is the file as it is, or a server made it from that one file and put a source map at its end (Vite in development does this). A bundle of many files has no coverage. Which page `page.goto('/path')` loads: - With no configuration: the file at that path from the project root. No server runs. The browser gets files as they are, so a page cannot load TypeScript: build first, or use a server. - With `"ubq": { "browser": { "url": "http://localhost:5173" } }` in `package.json`: the path at that URL. Add `"command": "npm run dev"` and `ubq` starts the server for a run that has a browser test, if nothing answers at the URL, and stops it after the run. `node ` does not start the server. - A full URL always works: `page.goto('https://example.com')`. A different browser version, or a shared download: - The browser version comes from the version of `playwright-core` in the project. - `PLAYWRIGHT_BROWSERS_PATH=` sets the folder of the download, for `ubq browser install` and for the tests. Use it for a cache that many CI jobs share. - `UBQ_BROWSER=` makes the tests use that program. No download is necessary then. ## Code for more than one runtime or system Some code runs only on Bun, or only on Windows. One run cannot run each line of such a project. The store keeps the coverage of each environment (a runtime and a system, such as `bun-darwin`), and `ubq uncovered` lists the code that no environment ran. `ubq uncovered` tells you when you need this: it prints a note if a range is on a line that names a runtime or a system (`process.platform`, `Bun`, `Deno`). On one machine, run the tests on each runtime. The second run does not remove the coverage of the first: ``` npx ubq on Node bun node_modules/.bin/ubq on Bun npx ubq uncovered code that no runtime ran ``` For other systems, each CI job keeps its store, and one job merges them. With GitHub Actions: ```yaml jobs: test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 - run: npm ci - run: npx ubq - uses: actions/upload-artifact@v7 with: name: store-${{ matrix.os }} path: .ubq/runs.db* include-hidden-files: true coverage: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 - run: npm ci - uses: actions/download-artifact@v8 with: pattern: store-* path: stores - run: npx ubq merge stores/* - run: npx ubq uncovered # exit code 1 if no job ran some code ``` - `ubq merge ...` takes a project folder, a `.ubq` folder, or a `runs.db` file. - Coverage of a file counts only if the file has the same content as now. Make each store from the same commit. - `ubq uncovered` prints the environments, if there is more than one: `coverage of: bun-darwin, node-linux`. - `ubq --changed` uses only the last run of each test. ## Tests that start a process A test can start the program under test as a child process: a command, or a server. The code of the project that the child runs is coverage of the test, as the code that the test process runs. So `ubq --changed`, `ubq uncovered`, and `ubq covers` work for code that only a child runs. No setup is necessary. ```ts import { spawnSync } from 'node:child_process' import { assert, test } from 'ubqtest' test('the command prints its version', () => { const r = spawnSync('node', ['src/cli.ts', '--version'], { encoding: 'utf8' }) assert.match(r.stdout, /^\d+\.\d+\.\d+\n$/) }) ``` - It works for a child on Node, Bun, or Deno, and for the processes that the child starts. For Deno, the test process must be Node, and the test must start `deno run`, `deno test`, or `deno serve` itself, not through a shell. - Stop a server with `child.kill()` in `cleanup`, and wait for its `close` event. The child writes its counts when it gets the signal. A child that ends with SIGKILL, and each child that is killed on Windows, can lose the counts of its last 200 ms. - ubq adds a module to `NODE_OPTIONS` and `BUN_OPTIONS` in the environment of the test process, so that each child loads it. A child with a new environment gets it too, if the runner is Node. - An error stack that the child prints has the correct lines. The columns can be too large, because the code has counters. ## Tests of a Cloudflare Worker The test file runs in workerd, the runtime of Workers, with the local bindings of the wrangler config (D1, R2, KV, Durable Objects, vars). The project must have `wrangler`. Tell ubq which test files are Workers tests, in `package.json`: ```json { "ubq": { "workers": [ { "config": "wrangler.jsonc", "include": ["test/**/*.test.ts"], "vars": { "API_KEY": "a test value" } } ] } } ``` `vars` is optional. Its values go on top of the `vars` of the wrangler config. ```ts import { env } from 'cloudflare:workers' import { assert, test } from 'ubqtest' import { executionContext, migrate } from 'ubqtest/workers' import worker from '../src/index.ts' await migrate('DB') // the files of migrations_dir, for the D1 binding DB test('lists the notes', async () => { const ctx = executionContext() const response = await worker.fetch(new Request('http://x/notes'), env, ctx) await ctx.settled() // the promises of ctx.waitUntil assert.deepEqual(await response.json(), []) }) ``` - `env` comes from the runtime (`cloudflare:workers`), not from ubq. It has the bindings of the wrangler config. - A test does not get the values of `.dev.vars` or `.env`. Those files can have real secrets. Give test values in `vars` of the `workers` entry. - A service binding to a Worker that is not this one gets a stand-in: `fetch` on it answers 503, and a call of a method throws. To test code that uses the service, give the handler an `env` object with your own value for that binding. - Only the test file can do I/O at its top level (`await migrate('DB')`, a query, a `fetch`). A module that the test file imports cannot: workerd does not permit I/O while a module loads. Export a function from that module, and await it at the top level of each test file. - Packages load as wrangler loads them: the `alias` key of the wrangler config works, and a CommonJS package gives its named exports. - A wrangler config with `containers` works, but no container starts in a test run. - `'ubqtest/workers'` has three functions: `executionContext()` makes the third argument of a handler, `scheduledController({ cron, scheduledTime })` makes the first argument of a `scheduled` handler, and `migrate(binding)` applies the D1 migrations. - Each test file gets a new workerd process with empty storage. The tests of one file share that storage. A write of one test is there for the next test of the file. - A project with more than one Worker has one entry in `workers` for each wrangler config. - Coverage works as for other tests: `ubq --changed`, `ubq uncovered`, and `ubq covers`. - `ubq` adds the `nodejs_compat` flag for the test run, because the test API uses modules of Node. - A Workers test file is not a plain program: `node ` cannot run it. Use `ubq `. Not available in a Workers test file: `testdir`, `mockImport`, `assert.snapshot`, and JSX files. A test that goes over its time limit fails, but the report does not list what the test left open. There is no `SELF` fetcher: call the handler of the Worker with a `Request`. ## Faster runs `ubq server` starts a background server for the project. It keeps test processes ready, so a run of a few files is about 30 ms faster, and the first run after an edit about 45 ms. `--repeat` is about 3 times faster. The commands, the output, and the exit codes do not change. It stops after 30 minutes with no use, or with `ubq server stop`. It is optional. ## Configuration Most projects need none. The `ubq` key of `package.json` can have `include` and `exclude` (lists of glob patterns for test files), `helpers` (glob patterns for test helpers: see below), `build` (glob patterns for build output and build tools: see below), `timeout` (milliseconds), `hangRerun` (`false` turns off the rerun of a test that timed out), and `bunInspectorCoverage` (`true` records coverage for a plain `bun `, with places that can be wrong. `ubq ` on Bun does not need it), and `browser` (see "Tests in a browser"), and `workers` (see "Tests of a Cloudflare Worker"). A folder with its own `package.json` is a different project: run `ubq` in that folder. At the root of a monorepo with no tests of its own, `ubq` runs each workspace. Build output and build tools are not source of the program. `ubq uncovered` does not list them. With no `build` key, they are the files in `dist/` and `scripts/` of the project. The key replaces that rule: `"build": ["out/**", "tools/**"]`. An empty list means that each file is source. If a test loads a built file that has a source map, the coverage is coverage of the source files that the map names: the result is the same as for a test that loads the source. After a change to a source file, build again before the tests run: until then, the run has no coverage of that file. A test helper is a file that is part of the tests and is not a test file: a setup module, or the scenario of an end-to-end test. `ubq uncovered` does not list its code, and `ubq mutate --diff` does not change it. Do not write tests for a helper. With no `helpers` key, a helper is each file in a folder with the name `test`, `tests`, or `__tests__`. The key replaces that rule: `"helpers": ["src/testing/**"]`. An empty list means that no file is a helper. A change to a helper still makes `ubq --changed` run the tests that loaded it. Code that no test can run can have a comment with the reason: `// ubq: not measured: `. Examples: a fallback for a value that is never absent, or code that runs before the coverage hooks start. Put the comment on a line of its own above the code, or at the end of the first line of the code. For a block, the first line in the block also works. `ubq uncovered` does not list that code, and it prints how many ranges it left out. A comment with no reason does not count. Look for a test first: the comment is for code that a test cannot reach. ## Requirements Node 24 or later, or Bun, or Deno. TypeScript runs with no build step, so the code must use only syntax that can be erased: no `enum`, no `namespace`, no parameter properties. In `tsconfig.json`, set `"allowImportingTsExtensions": true`, `"noEmit": true`, and `"erasableSyntaxOnly": true`. Bun and Deno also run `.tsx` and `.jsx` files, with coverage. Node does not run them. Tests of a Cloudflare Worker run in workerd, and the project must have `wrangler`: see "Tests of a Cloudflare Worker". --- # ubqtest API Each export of the `ubqtest` package, with its type and its doc comment. `npm run docs` makes this file from the type declarations of the built package. Do not change it by hand. A test fails if it is not current. For how to use the API, with examples and the rules of a test, read [GUIDE.md](guide.md) (`ubq guide`). ## Functions ### `assert` ```ts const assert: Assert; ``` ### `isDeepEqual` True if the two values are deeply equal, by the rules of `assert.deepEqual`: the same structure, values, and prototypes. It is not an assertion. Use it for a condition that `assert.deepEqual` cannot state, such as "the list has an element that equals this": `assert(list.some(item => isDeepEqual(item, expected)))`. ```ts const isDeepEqual: (a: unknown, b: unknown) => boolean; ``` ### `fakeClock` Replaces the timers and the clocks of the process with a clock that moves only when the test moves it. The real ones come back when the test ends, or at `clock.restore()`. ```ts const clock = fakeClock() const done = spy() setTimeout(done, 1000) await clock.tick(1000) assert.equal(done.calls.length, 1) ``` ```ts const fakeClock: (options?: FakeClockOptions) => FakeClock; ``` ### `cleanup` Runs `fn` when the test ends. Outside a test: when the file ends. The functions run in reverse order. An error in one fails the test. ```ts const cleanup: (fn: CleanupFn) => void; ``` ### `env` Sets environment variables, or deletes them with `undefined`. The old values come back when the test ends. ```ts const env: (vars: Record) => void; ``` ### `mockImport` Loads a new instance of a module. In that instance, and in the project modules that it imports, each dependency in `mocks` is replaced by the given exports. ```ts const { greet } = await mockImport(() => import('../src/greet.ts'), [ [() => import('../src/db.ts'), { fetchUser }], ]) ``` ```ts const mockImport: (subject: Loader, mocks: { [K in keyof D]: [Loader, MockOf]; }) => Promise; ``` ### `spy` A function that records each call in `calls`, and then calls `impl`. ```ts const spy: void>(impl?: F) => Spy; ``` ### `spyOn` Replaces the method `key` of `object` with a spy. The spy calls `impl`, or the original method. The original comes back when the test ends. ```ts const spyOn: >(object: T, key: K, impl?: T[K]) => Spy; ``` ### `describe` ```ts const describe: Describe; ``` ### `test` ```ts const test: Test; ``` ### `testdir` Makes a new folder with `files` in it and returns its path: `.ubq/testdir//`. The folder is removed when the test ends. If the test fails, the folder stays. ```ts const testdir: (files?: Files) => string; ``` ### `browser` Import from `'ubqtest/browser'`. The project needs the `playwright-core` package, and `ubq browser install`. A new page of a browser (Chromium), as a `Page` of Playwright. Use the API of Playwright for all work with the page. The page closes when the test ends. An error that the page does not catch fails the test. `page.goto('/a/page.html')` loads a file of the project, from the project root. If the configuration has `browser.url`, the path is from that URL. `options` are the options of `browser.newContext` of Playwright, such as `viewport` or `locale`. ```ts const browser: (options?: BrowserContextOptions) => Promise; ``` ### `executionContext` Makes the third argument of a handler: `worker.fetch(request, env, ctx)`. `await ctx.settled()` waits for the promises that the handler gave to `ctx.waitUntil`. The test also waits for them when it ends. ```ts const executionContext: () => ExecutionContext & { settled(): Promise; }; ``` ### `scheduledController` Makes the first argument of a `scheduled` handler. ```ts const scheduledController: (options?: { scheduledTime?: number; cron?: string; }) => ScheduledController; ``` ### `migrate` Applies the migrations of a D1 database to its local copy. `binding` is the name of the database in the wrangler config, such as `'DB'`. The files come from its `migrations_dir`. ```ts const migrate: (binding: string) => Promise; ``` ## Types ### `Assert` ```ts interface Assert { /** Passes if `value` is truthy. Narrows the type of `value`. */ (value: unknown, message?: string): asserts value; /** Passes if `Object.is(actual, expected)`. Narrows the type of `actual`. */ equal(actual: T, expected: E, message?: string): asserts actual is E; /** Passes if not `Object.is(actual, expected)`. */ notEqual(actual: T, expected: NoInfer, message?: string): void; /** Passes if both have the same structure, values, and prototypes. */ deepEqual(actual: T, expected: NoInfer, message?: string): void; /** * Passes if `actual` has the shape of `pattern`. Each key in the * pattern must match. Extra keys in `actual` are fine. A RegExp * tests a string. */ match>(actual: T, pattern: P, message?: string): void; /** * Passes if `fn` throws an error that fits `expected`: a RegExp * tests the message, a class tests `instanceof`, an object is a * `match` pattern. Returns the error. */ throws(fn: () => unknown, expected: ErrorMatch, message?: string): Error; /** As `throws`, for a promise. The result must be awaited. */ rejects(promise: Promise | (() => Promise), expected: ErrorMatch, message?: string): Promise; /** * As `match`, for a value that changes with time, such as the text * of a page. Calls `fn` again and again until its value matches * `pattern`. Fails after 2000 ms with the last value. The result * must be awaited. */ eventually>>(fn: () => T, pattern: P, message?: string): Promise; /** * Passes if the text of `value` is the text in the snapshot file * of the test file. `ubq --update ` writes the file. */ snapshot(value: unknown, message?: string): void; /** Fails the test. */ fail(message: string): never; toMatchSnapshot: Hint<'Use assert.snapshot(value)'>; matchSnapshot: Hint<'Use assert.snapshot(value)'>; ok: Hint<'Use assert(value)'>; strictEqual: Hint<'Use assert.equal (it is always strict)'>; deepStrictEqual: Hint<'Use assert.deepEqual (it is always strict)'>; } ``` ### `Pattern` A partial shape: listed keys must match, and a RegExp tests a string. ```ts type Pattern = Prim> | Obj>; ``` ### `FakeClock` ```ts interface FakeClock { /** The time of the clock, in milliseconds since 1970. */ readonly now: number; /** The count of timers that wait. */ readonly timers: number; /** * Moves the clock forward by `ms`. Each timer that is due runs, in * the order of its time. Promise callbacks run after each timer. */ tick(ms: number): Promise; /** Moves the clock from timer to timer until no timer waits. */ runAll(): Promise; /** Puts the real timers and clocks back. The end of the test does this too. */ restore(): void; [Symbol.dispose](): void; } ``` ### `FakeClockOptions` ```ts interface FakeClockOptions { /** The time at the start. The default is the real time. */ now?: number | Date; } ``` ### `Files` File names with their content. A nested object is a folder. ```ts interface Files { [name: string]: string | Uint8Array | Files; } ``` ### `MockOf` The exports that replace a module. Each one must have the type of the real export. For a function with overloads, a function that fits the last overload is correct too. ```ts type MockOf = { [K in keyof T]?: T[K] extends AnyFn ? T[K] | ((...args: Parameters) => ReturnType) : T[K]; }; ``` ### `Spy` A function that records each call. ```ts type Spy = F & { calls: SpyCall[]; }; ``` ### `SpyCall` One call of a spy. `error` is present if the call threw. ```ts interface SpyCall { args: Parameters; result?: ReturnType; error?: unknown; } ``` ### `TestContext` ```ts interface TestContext { /** Aborts when the test times out. */ readonly signal: AbortSignal; } ``` ### `TestFn` ```ts type TestFn = (ctx: TestContext) => void | Promise; ``` ### `TestOptions` ```ts interface TestOptions { /** The reason to skip the test. `false` runs the test. */ skip?: string | false; /** Milliseconds. The default is 5000. */ timeout?: number; } ``` --- # ubq guide: convert How to move the tests of a project from tap, `node:test`, vitest, jest, or mocha to `ubqtest`. This text comes from the installed package. `ubq guide convert` prints it. Read `ubq guide` first: it has the API and the rules of ubq. ## The steps 1. **Record the old result.** Run the old test command. Write down the count of tests that pass, and each test that fails or is skipped. You compare with this at the end. 2. **Install.** `npm install --save-dev ubqtest`. Then `npx ubq doctor`, and do each `FIX` line. A line that says that `ubq convert` does the work needs no action in this step. 3. **Convert.** `npx ubq convert test` (give the folders and files that have tests). `--dry` shows the report and changes no file. The command changes the files in place. Make sure that `git status` is clean before it, so that `git diff` shows the changes. It is safe to run the command again on one file. 4. **Do each TODO.** The command writes `// TODO(ubq): ...` above each statement that it cannot change. It makes only the changes that are correct in each case, so a TODO can be easy to do by hand: you can see what the code around it does, and the command cannot. `grep -rn 'TODO(ubq)' test` lists them. The sections below tell how to do each kind. Remove the comment when its statement is done. 5. **Run.** `npx ubq`. Do one failure at a time: `npx ubq :` runs one test. Read "Failures after the conversion" below before you change a test. 6. **Compare.** The count of tests that pass must be the count of step 1, or you must know the reason for each difference. A test that passed before and is skipped or removed now is a defect of the conversion. 7. **Clean up.** Set `"test": "ubq"` in the `scripts` of `package.json`. Remove the old test tool, its plugins, and its configuration (`.taprc`, `vitest.config.*`, `jest.config.*`, `.mocharc.*`, `c8` and `nyc` settings), and the entries for the old tool in `types` of `tsconfig.json`. Change the test command of CI. The folder `.ubq/` has the run store, and git ignores it with no setup. Commit `ubq-snapshots/`. Do not change the source of the project to make a test pass, unless the test found a defect. Do not make an assertion weaker. ## What `ubq convert` changes - The import of the old tool becomes `import { ... } from 'ubqtest'`, with the names that the file uses. - `it` becomes `test`. `describe` stays. - An assertion with a certain form in ubq gets that form. The tables below list them. - A message of an assertion becomes a comment at the end of its line. ubq prints the source line of a failed assertion, so the comment is in the failure report. - `t.end()` and `t.plan(n)` go away. A test of ubq ends when its function ends. - A promise that an assertion needs gets `await`, and its function becomes `async`. - A tap test with subtests becomes `describe`. It does not change the name of a file or the source of the project. It changes `package.json` in one case: for a project of the vitest plugin for Workers, it writes the `workers` key of the ubq configuration, if the key is not there. The report has one line for each TODO, with the number of a hint. The full text of each hint is below the files, one time. ## Names of test files ubq runs files whose names end in `.test.ts` (or `.test.js`, `.test.mts`, `.test.tsx`, and so on). For tests with different names, do one of these: - Rename them: `test/parse.ts` to `test/parse.test.ts`. - Add the pattern to `package.json`: `"ubq": { "include": ["test/**/*.ts"], "exclude": ["test/fixtures/**", "test/helpers/**"] }`. Each file that `include` finds must be a test file. A helper file with no tests is an error ("the file registered no tests"): put it in `exclude`. ## Imports of the source - A test can import `'../src/parse.js'` when the file is `parse.ts`. ubq finds the `.ts` file. - A test that imports the build output (`'../dist/index.js'`) works only after a build. Import the source (`'../src/index.ts'`): then a run needs no build, and coverage has the source lines. - Node runs TypeScript with the types removed, and no other change. Source with `enum`, `namespace`, or a parameter property (`constructor(private x: number)`) does not load. `ubq doctor` checks `erasableSyntaxOnly`. If the project has such syntax, tell the user: the choice is to change that syntax or to run the tests on Bun (`bun node_modules/.bin/ubq`). ## tap | tap | ubq | | --- | --- | | `t.test(name, async t => { ... })` | `test(name, async () => { ... })` | | `t.equal(a, b)`, `t.is`, `t.strictEqual` | `assert.equal(a, b)` | | `t.not(a, b)`, `t.notEqual` | `assert.notEqual(a, b)` | | `t.same(a, b)`, `t.strictSame`, `t.deepEqual` | `assert.deepEqual(a, b)` | | `t.ok(a)`, `t.notOk(a)` | `assert(a)`, `assert(!a)` | | `t.match(a, pattern)`, `t.has` | `assert.match(a, pattern)` | | `t.throws(fn, expected)` | `assert.throws(fn, expected)` (`Error` if there was none) | | `await t.rejects(p, expected)` | `await assert.rejects(p, expected)` | | `t.resolveMatch(p, pattern)` | `assert.match(await p, pattern)` | | `t.type(a, 'string')`, `t.type(a, Class)` | `assert.equal(typeof a, 'string')`, `assert(a instanceof Class)` | | `t.error(err)` | `assert(!err)` | | `t.pass()`, `t.fail(msg)` | `assert(true)`, `assert.fail(msg)` | | `t.matchSnapshot(a)` | `assert.snapshot(a)` | | `t.teardown(fn)`, `t.after(fn)` | `cleanup(fn)` | | `t.testdir({ ... })` | `testdir({ ... })` | | `t.mockImport('../a.js', { '../b.js': { f } })` | `mockImport(() => import('../a.js'), [[() => import('../b.js'), { f }]])` | | `t.comment(...)` | `console.log(...)` | | `t.end()`, `t.plan(n)` | removed | Things to check by hand: - **`t.same` and `t.match` are loose in tap.** `t.same` compares with `==` for values and ignores the prototype. `assert.deepEqual` is strict. If a converted `deepEqual` fails on a difference such as `1` and `'1'`, or a class instance and a plain object, the old test was loose. Change the expected value to the exact value. Use `assert.match(a, { ... })` when the test must check only some keys. - **A test with subtests.** ubq has no test in a test. The outer test becomes `describe`. Code in the outer test that is not in a subtest ran one time, before the subtests. In a `describe`, that code runs when the file loads. Setup that makes values is fine there. Move each assertion into a `test`. Move setup that must be new for each test into a function, and call it in each test. - **Assertions at the top level of the file.** Put them in a `test('name', () => { ... })`. - **`t.plan(n)`.** A plan made sure that callbacks ran. With `async` and `await`, the test function waits for them. If the test has a callback API, wrap it in a promise and `await` it. A test with zero assertions fails in ubq, so a callback that never runs is found. - **`t.beforeEach`, `t.afterEach`, `t.before`.** See "Hooks". - **`t.capture`, `t.intercept`, `t.captureFn`.** Use `spyOn(object, key, impl?)`, then read `spy.calls`. - **`t.mockImport` with keys or values that are not literals, `t.mockRequire`, `t.mock`.** Write `mockImport` by hand. The mock must have the types of the real exports. `mockRequire` of a CommonJS module: import the module with `mockImport` in the same way. - **`t.context`.** Make the values in a setup function and return them. - **Snapshots.** tap keeps them in `tap-snapshots/`. ubq keeps them in `ubq-snapshots/`, in a different form. When the tests pass with the old tool, run `npx ubq --update ` for each file with `assert.snapshot`, read the new files, and remove `tap-snapshots/`. - **`t.skip`, `{ skip: true }`, `{ todo: true }`.** Give the reason as text: `test(name, { skip: 'needs a GPU' }, fn)`. - **Coverage settings.** tap fails a run with less than full coverage. ubq has no such limit. `npx ubq uncovered` lists the code that no test ran. - **A fixture or helper that uses `t`.** A helper function that gets `t` as an argument must import `assert` from `ubqtest` and must not get `t`. ## node:test and node:assert | node | ubq | | --- | --- | | `it`, `test` from `node:test` | `test` | | `assert.strictEqual`, `assert.equal` | `assert.equal` | | `assert.deepStrictEqual`, `assert.deepEqual` | `assert.deepEqual` | | `assert.notStrictEqual` | `assert.notEqual` | | `assert.ok(a)` | `assert(a)` | | `assert.throws(fn)`, `assert.rejects(p)` | the same with `Error` as the expected error | | `assert.ifError(err)` | `assert(!err)` | | `mock.fn(impl)`, `mock.method(object, key)` | `spy(impl)`, `spyOn(object, key)` | Things to check by hand: - **`assert.equal` and `assert.deepEqual` from `node:assert` (not strict) are loose.** In ubq they are strict. - **`assert.doesNotThrow(fn)`.** Call the function, and assert on its result. - **`assert.notDeepStrictEqual`, `assert.doesNotMatch`.** Write the condition: `assert(!pattern.test(text))`. - **The test object `t`.** `t.mock.fn()` is `spy()`. `t.after(fn)` is `cleanup(fn)`. `t.test(...)` is a subtest: use `describe`. `t.diagnostic(text)` is `console.log(text)`. `t.skip()` in a test: use `{ skip: condition && 'the reason' }`. - **`before`, `after`, `beforeEach`, `afterEach`.** See "Hooks". - **`mock.timers`.** Use `fakeClock`. ## vitest and jest | vitest, jest | ubq | | --- | --- | | `it`, `test` | `test` | | `expect(a).toBe(b)` | `assert.equal(a, b)` | | `expect(a).toEqual(b)`, `toStrictEqual` | `assert.deepEqual(a, b)` | | `expect(a).not.toBe(b)` | `assert.notEqual(a, b)` | | `toBeTruthy()`, `toBeFalsy()` | `assert(a)`, `assert(!a)` | | `toBeNull()`, `toBeUndefined()`, `toBeDefined()` | `assert.equal(a, null)`, `assert.equal(a, undefined)`, `assert.notEqual(a, undefined)` | | `toContain(b)`, `toHaveLength(n)` | `assert(a.includes(b))`, `assert.equal(a.length, n)` | | `toMatch(/re/)`, `toMatch('text')` | `assert.match(a, /re/)`, `assert(a.includes('text'))` | | `toMatchObject(o)` | `assert.match(a, o)` | | `toBeInstanceOf(C)` | `assert(a instanceof C)` | | `toBeGreaterThan(b)` and the other comparisons | `assert(a > b)` | | `expect(fn).toThrow(x)` | `assert.throws(fn, x)` (a text becomes a RegExp) | | `await expect(p).rejects.toThrow(x)` | `await assert.rejects(p, x)` | | `await expect(p).resolves.toEqual(b)` | `assert.deepEqual(await p, b)` | | `toHaveBeenCalled()`, `toHaveBeenCalledTimes(n)` | `assert(s.calls.length > 0)`, `assert.equal(s.calls.length, n)` | | `toHaveBeenCalledWith(...args)` | `assert.deepEqual(s.calls.at(-1)?.args, [...args])` | | `toMatchSnapshot()` | `assert.snapshot(a)` | | `vi.fn(impl)`, `jest.fn(impl)` | `spy(impl)` | | `vi.spyOn(object, key)` | `spyOn(object, key)` | | `it.skip(name, fn)` | `test(name, { skip: 'the reason' }, fn)` | | `it.todo(name)` | `test(name, { skip: 'todo' }, () => {})` | | `it(name, fn, 10000)` | `test(name, { timeout: 10000 }, fn)` | Things to check by hand: - **`toEqual` is loose.** It ignores keys with the value `undefined` and ignores the class. `assert.deepEqual` is strict. Use the exact value, or `assert.match` for some keys. - **`toHaveBeenCalledWith`.** jest passes if any call had the arguments. The converted line checks the last call. If the call is not the last one, use `assert(s.calls.some(c => ...))` or the index of the call. - **`expect.any`, `expect.objectContaining`, `expect.anything`.** `assert.match` with a partial object does the work of `objectContaining`. For `any(String)`, assert on `typeof`. - **`toContainEqual(b)`.** The command writes `assert(list.some(item => isDeepEqual(item, b)))`. `isDeepEqual` gives true or false by the rules of `assert.deepEqual`. Use it also for `expect.arrayContaining` with objects. - **`it.each(table)`.** A `for` loop around `test()`. Each test name must be different: put the value in the name. - **`.only`.** The command removes it. ubq runs one test from the command line. - **`vi.mock(path)`, `jest.mock(path)`.** These change the module for the whole file. `mockImport` gives a new instance of the module under test with its dependency replaced, in one test: ```ts const { checkout } = await mockImport(() => import('../src/cart.ts'), [ [() => import('../src/payment.ts'), { charge: spy(async () => ({ id: 'r1' })) }], ]) ``` The test then uses `checkout` from that instance, not from the import at the top of the file. - **`mockReturnValue`, `mockResolvedValue`, `mockImplementation`.** A spy gets its implementation when it is made: `spy(() => 1)`, `spy(async () => value)`. The command converts a call that is on the line that makes the spy. For a call on a later line, or for a different result in each call, write the logic in the implementation. In one test, a second `spyOn` on the same key replaces the first. - **`fn.mock.calls[0]`** is `fn.calls[0]?.args`. `fn.mock.results[0].value` is `fn.calls[0]?.result`. - **`vi.restoreAllMocks()`, `mockRestore()`.** A `spyOn` spy goes back when its test ends. The command removes a hook that only restores, and a call at the end of a test. If a helper of the project gives a restore function (`restore = m.restore`, with `afterEach(() => restore())`), delete the hook and each assignment. Remove the other calls. ubq has no call that puts the method back before the test ends. - **`vi.useFakeTimers()`, `vi.advanceTimersByTime(ms)`.** `const clock = fakeClock({ now: 0 })`, then `await clock.tick(ms)`. The clock goes back when the test ends. - **`beforeEach` and the other hooks.** See "Hooks". - **Globals.** jest gives `describe`, `it`, and `expect` with no import. ubq has no globals. The command adds the import. - **A DOM (jsdom, happy-dom), a custom environment, a transform.** ubq has none. Tell the user about each test file that needs one. - **A Cloudflare Worker (`cloudflare:test`, `@cloudflare/vitest-pool-workers`).** These tests run in the Workers runtime with ubq too. Read "Tests of a Cloudflare Worker" in `ubq guide`. The command reads `vitest.config.*` of the project and writes the `workers` key to `package.json`: one entry for each project of the config, with its wrangler config, its test files, and the plain values of `miniflare.bindings` as `vars`. Read the notes that it prints: a value that is not plain must go into `vars` by hand. A binding for the D1 migrations (`TEST_MIGRATIONS: await readD1Migrations(...)`) gets no note and is not in `vars`: `migrate` does that work. If the project has a copy of its wrangler config for the tests, which leaves out `containers` or a service binding, the copy is not necessary: ubq loads a config with them. If a setup file of the config only applied D1 migrations, the command adds `await migrate('DB')` to each test file of that project, and says that the setup file can go. The command also changes these names of `cloudflare:test`: | vitest plugin | ubq | | --- | --- | | `env` from `cloudflare:test` | `env` from `cloudflare:workers` | | `createExecutionContext()` | `executionContext()` | | `await waitOnExecutionContext(ctx)` | `await ctx.settled()` | | `createScheduledController(options)` | `scheduledController(options)` | | `applyD1Migrations(env.DB, env.TEST_MIGRATIONS)` | `await migrate('DB')` | ubq has no `SELF`, `fetchMock`, `runInDurableObject`, or the other names: write the test without them. Values that the vitest config gave to the tests (`miniflare.bindings`) go in `vars` of the `workers` entry in the ubq configuration. The plugin gave each test its own storage. ubq gives each test file its own storage. A setup file of the vitest config (`setupFiles`) becomes a module that exports a function, and each test file awaits that function at its top level. The module cannot do the work when it loads: workerd permits I/O at the top level only in the test file itself. For D1 migrations, the line is `await migrate('DB')` in each test file. ## mocha The command converts `describe` and `it` with `node:assert`. It does not convert `chai` or `sinon`. Write those assertions by hand with the tables above. `this.timeout(ms)` becomes `test(name, { timeout: ms }, fn)`. ## Hooks ubq has no `beforeEach`, `afterEach`, `beforeAll`, or `afterAll`. - **Setup for each test.** Make a function that does the setup and returns what the test needs. Call it in each test. ```ts // before let db: Db beforeEach(async () => { db = await open(':memory:') }) afterEach(() => db.close()) it('adds', () => { db.add(1); expect(db.count()).toBe(1) }) // after const setup = async () => { const db = await open(':memory:') cleanup(() => db.close()) return db } test('adds', async () => { const db = await setup() db.add(1) assert.equal(db.count(), 1) }) ``` - **Work after each test.** `cleanup(fn)` in the test or in its setup function. It runs when the test ends, also if the test fails. - **Setup one time for the file.** Code at the top level of the file, before the first `test`. `cleanup`, `testdir`, `env`, and `spyOn` at the top level last until the file ends. Top-level `await` works. A `beforeAll` callback runs after the whole file loaded. Code at the top level runs at its place, so put it after the declarations that it uses. The command changes a `beforeAll` at the top level to a block (`{ ... }`) at the same place, if the callback uses no name that the file declares below it. A block keeps the local names of the callback out of the file. In a Workers test file, the block must come after `await migrate(...)`. - **Setup one time for a group.** A `describe` function cannot `await`. Make a function that does the work one time, and await it at the start of each test of the group: ```ts let ready: Promise | undefined const setup = () => (ready ??= prepare()) test('first', async () => { await setup() }) ``` Tests that need the result of an earlier group in a shared database are fragile. A test that makes its own rows is better. - **Setup one time for all files.** ubq has none. Each test file is its own process. Put the setup in a module, and import it in each test file that needs it. ## Failures after the conversion Find the kind of the failure before you change a line. | The report says | The cause | Do this | | --- | --- | --- | | `the test ran no assertions` | The old tool passed a test with no check. | Add the assertion that the test name promises. If the test only checks that a call does not throw, assert on its result. | | `expected values to be deeply equal`, with a difference of type or prototype | The old assertion was loose. | Make the expected value exact, or use `assert.match`. | | `assert.rejects was not awaited` | The old tool waited for it. | Add `await`. | | `after the test ended: ...` | The test starts work and does not wait for it. | `await` the work. This was a defect in the old test. | | `the test changed the environment variable ...` | The test sets `process.env` and does not put it back. | `env({ NAME: 'value' })`. | | `the test did not end in 5000 ms`, with `still open: ...` | The test leaves a server, a timer, or a process open. | Close it with `cleanup`. The `HANG` block names the line that opened it. | | `duplicate test name` | Two tests of one group have one name. | Make the names different. | | `the test file did not load` | A syntax error, an import that names no file, or syntax that Node cannot run. | Read the line in the report. See "Imports of the source". | | `the file registered no tests` | The file is a helper, not a test file. | Put it in `"exclude"`. | | a `TYPE` block | The type checker runs with each run. | Correct the type. A mock must have the types of the real export. | | a `TYPE` block at a converted assertion, such as `Argument of type 'string \| undefined' is not assignable to parameter of type 'string'` | `expect()` took a value of each type. `assert` is strict: the actual value cannot be `undefined` where a method is called on it, and the expected value must have the type of the actual value (the parameter is `NoInfer`). | Add `!` or `?.` to the actual value, or narrow it first with `assert(value)`. If the two types differ on purpose, write `assert.deepEqual(a, b)`. | A test that fails in ubq for one of the first six causes was not correct before: it passed for the wrong reason. Correct the test. Do not skip it. ## When you are done Report to the user: - the count of tests before and after, and the reason for each difference - each test that you changed in more than its form, and why - each thing that ubq does not have and the project used (a DOM, a coverage limit, a global setup), with what you did - each `TODO(ubq)` that is still in the code --- # Configuration The goal is as little configuration as possible (D24). Most projects need none. ## What ubq runs with no configuration `ubq` with no arguments runs each file in the project whose name ends in `.test.ts`, `.test.mts`, `.test.cts`, `.test.tsx`, `.test.js`, `.test.mjs`, `.test.cjs`, or `.test.jsx` (D19). The project is the folder of the nearest `package.json` at or above the working directory. The search leaves out: - `node_modules` - folders whose names start with a dot - folders that have their own `package.json` (D23). Such a folder is a different project, with its own run store. Examples: a package of a monorepo, a folder of experiments, a fixture project. A file that is an argument always runs: `ubq any/file.ts`. A folder that is an argument runs the test files of the project that are in it. A folder that is a different project is an error. ## The `ubq` key of `package.json` ```json { "ubq": { "include": ["test/**/*.ts"], "exclude": ["test/helpers/**"], "timeout": 10000 } } ``` | Key | Type | Default | Meaning | | --- | --- | --- | --- | | `include` | list of glob patterns | the `.test.` name pattern | Replaces the name pattern. Paths are from the project root. | | `exclude` | list of glob patterns | none | Files that are not test files, after `include`. | | `helpers` | list of glob patterns | `**/{test,tests,__tests__}/**` | Test helpers: files that are part of the tests and are not test files, such as a setup module or the scenario of an end-to-end test (D51). `ubq uncovered` leaves them out, and `ubq mutate --diff` does not change them. The list replaces the default. An empty list means that no file is a helper. | | `build` | list of glob patterns | `dist/**`, `scripts/**` | Files that are not source of the program: the output of a build, and the tools of the build (D55). `ubq uncovered` leaves them out, and `ubq mutate --diff` does not change them. The list replaces the default. An empty list means that each file is source. A built file with a source map gives its coverage to the source files that the map names. | | `timeout` | number | 5000 | The time limit of a test in milliseconds, if the test and its groups set none. | | `hangRerun` | boolean | true | `false` turns off the rerun of a test that timed out with something open (D14). | | `bunInspectorCoverage` | boolean | false | `true` records coverage for a test file that runs as a plain program on Bun (`bun test/foo.ts`). The places are a guess and can be wrong (D39). | | `browser` | object | none | For tests that use `'ubqtest/browser'` (D45). `url`: the base of `page.goto('/path')`. `command` (optional): the shell command that starts the server at `url`. `ubq` runs it from the project root if a test file of the run imports `'ubqtest/browser'` and nothing answers at `url`, waits 30 seconds at most for an answer, and stops it after the run. Its output is in `.ubq/browser-server.log`. With no `browser` key, a page loads the files of the project from the disk. | | `workers` | list of objects | none | Test files that run in workerd, the runtime of Cloudflare Workers (D48). Each object has `config` (the path of a wrangler config, from the project root) and `include` (glob patterns of test files, from the project root). `vars` (optional) is an object of test values for vars: they go on top of the `vars` of the wrangler config. The values of `.dev.vars` and `.env` are not read. A test file that matches gets the bindings of that config. A file must also be a test file by `include` and `exclude` of the first rows. The first object that matches a file wins. The project must have the `wrangler` package. | Rules: - A key that is not in the table is an error (exit code 4). The message lists the keys. - A value of the wrong type is an error (exit code 4). The message gives an example of a correct value. - The three always-left-out folders above stay left out with `include`. - Patterns have no `!` negation. Use `exclude`. - There is no config file and no config module. JSON has no logic, each tool can read it, and ubq reads `package.json` in any case. Source: `src/config.ts`. Tests: `test/select.test.ts`. ## A monorepo At a project root that has `workspaces` in its `package.json` and no test files of its own, `ubq` runs the command in each workspace, one after the other (`ubq`, `ubq --failed`, `ubq --changed`). Each workspace is a project with its own store, and the paths in its report are from its own folder. The last line is `workspaces: 3 passed`, or `workspaces: 1 of 3 failed: packages/b`. A workspace with no tests does not count. For a question about stored runs, or for one test, run `ubq` in the folder of the workspace. --- # use-ubq ubq is a test tool for TypeScript and JavaScript, made for coding agents. The package is `ubqtest`. The command is `ubq`. The package has its own guides, and they are correct for the installed version. Read them before you write a test or run a command. Do not work from memory of a different test tool: ubq has no `it`, no `expect`, no hooks, and no `.only`. ``` npx ubq guide the API, the rules, and the commands npx ubq guide convert how to move a project from a different test tool npx ubq doctor checks the setup, and prints one FIX line for each problem ``` If `ubqtest` is not installed: `npm install --save-dev ubqtest`. On Bun, the command is `bun node_modules/.bin/ubq`. ## To write or correct tests 1. `npx ubq guide` 2. Write the test. Each name comes from `'ubqtest'`. 3. `npx ubq `. A pass prints one line. A failure block has the place, the source line, the difference, and a `rerun:` command for that one test. 4. After a change to the source: - `npx ubq --changed` runs the tests that ran the changed code. - `npx ubq uncovered --diff` lists the changed code that no test runs. - `npx ubq mutate --diff` tells if the tests of the changed lines are strong. For code that runs in a browser, the guide has "Tests in a browser": `browser()` from `'ubqtest/browser'` gives a Playwright `Page`. Do not add jsdom or a different driver. If `ubq uncovered` lists code for a different runtime or system, the guide has "Code for more than one runtime or system": run the tests in each environment, and merge the stores with `ubq merge`. Do not write a test that fakes the platform to reach that code, if a real run can reach it. A `// ubq: not measured: ` comment is only for code that no test can reach. For a Cloudflare Worker, the guide has "Tests of a Cloudflare Worker": the `workers` key of the configuration makes test files run in workerd, with the bindings of the wrangler config. ## To convert a project 1. `npx ubq guide convert`. Do its steps in order. 2. The command `npx ubq convert ` makes the certain changes and writes `// TODO(ubq): ...` where you must decide. 3. A test that passed with the old tool and fails with ubq is usually a test that passed for the wrong reason (no assertion, a promise that nothing waited for, a loose comparison). The guide has a table of these. Correct the test. Do not skip it, and do not make its assertion weaker. 4. Report to the user what the guide lists in "When you are done".