mrkeyoor.com_
Sat 08 Aug 21:00 UTC
npmTestingupdated 08 Aug 2026

uvu

uvu is a small Node.js test runner built around self-contained test files. You register tests on an unnamed `test` suite or named suites, attach before and after hooks, use mutable per-suite context, and explicitly call `run()`. The CLI finds matching files and queues their suites, while individual files can run directly with Node. Its optional assertion module provides strict, deep, type, pattern, snapshot, fixture, and error checks.

Verdict

uvu remains pleasant for small, fast Node test suites whose needs fit its compact surface. New applications that expect watch, coverage, mocks, transforms, or browser environments will usually spend less time wiring Vitest or another fuller runner.

API stability4/5The 0.5 API is small and has remained unchanged for years: suites are callable, hooks and context are explicit, `run()` queues work, and assertions throw ordinary Error subclasses. Both require and import entries are published. The version is still below 1.0, however, and long inactivity means stability comes partly from lack of change rather than an active compatibility policy.
Docs4/5The README and focused documents cover suite choice, run behavior, all hooks, mutable typed context, assertions, CLI discovery and regex matching, direct-file isolation, ESM loading, coverage, and watch integration. The limitations are documented honestly. Some examples retain legacy `-r esm`, old Node benchmarks, and older dependency versions, which dates the guidance.
Maintenance2/5Version 0.5.6 was published on July 3, 2022 and the repository was last pushed on August 30, 2024. GitHub shows 88 open issues and pull requests. The existing package still functions and its narrow API reduces churn, but the release gap and backlog mean modern Node, TypeScript loader, coverage, and ecosystem integration problems should not be expected to move quickly.
Ecosystem3/5uvu recorded 5,179,663 npm downloads in the latest complete week, has 3,032 GitHub stars, supports ESM and CommonJS, and works with any assertion library because failures are thrown Errors. Its external-tool philosophy makes c8, nyc, watchers, loaders, and DOM shims usable, but those integrations are recipes rather than a coordinated plugin ecosystem.

Use it if

  • You want very low test-runner startup overhead for a small Node library or command-line project
  • You value test files that can run directly with Node as well as through a collector CLI
  • A compact suite, hook, context, and assertion API is enough for your project
  • You need both CommonJS and ESM entry points without committing to a large framework
Skip it if

Setup reality

`npm install --save-dev uvu` installs version 0.5.6 plus dequal, diff, kleur, and sade. The package declares Node 8 or newer and exposes both CommonJS and ESM entries, but your own syntax still has to match the Node version and package module mode. A test file imports `test` or creates a `suite`, registers callbacks, and must call `.run()`; forgetting that final call produces a file that loads without joining the run queue. Put a script such as `uvu tests` in package.json. The CLI does not use globs. With no arguments it applies its built-in regular expression to common test directories and filename suffixes; when a directory is supplied, an optional second argument becomes a case-insensitive regular expression. Ignore flags are regular expressions too. The CLI recognizes TypeScript extensions during discovery but does not transpile TypeScript by itself, so preload a compatible runtime such as `ts-node/register` or compile first. The bundled assertions are optional and failures are simply thrown Errors, which makes Node's assert or another library valid. `assert.throws` is synchronous; the docs recommend an explicit try/catch pattern for rejected promises. Snapshots are inline expected strings rather than an automatically updated snapshot-file system. There is no watch process or coverage engine; add a watcher and c8 or nyc yourself. Browser-compatible code and assertions do not turn the CLI into a browser runner, so DOM tests still need jsdom or an actual browser test tool. Hooks always run cleanup after failures, but suite context is mutable and shared across tests, so reset it deliberately.

Patterns

Write and run a basic test filewrite-basic-test

import { test } from 'uvu';
import * as assert from 'uvu/assert';

test('adds two values', () => {
  assert.is(2 + 3, 5);
});

test.run();

The exported `test` is an unnamed suite. The file registers nothing with uvu until `test.run()` is called.

Group related tests in a named suitegroup-named-suite

import { suite } from 'uvu';
import * as assert from 'uvu/assert';

const Users = suite('users');
Users('normalizes email', () => {
  assert.is(normalizeEmail(' A@EXAMPLE.COM '), 'a@example.com');
});
Users.run();

Each named suite needs its own `run()` call, even when several suites live in one file.

Set up and clean up suite resourcesuse-suite-hooks

const Api = suite('api');

Api.before(async context => { context.server = await startServer(); });
Api.after(async context => { await context.server.close(); });
Api.before.each(context => { context.requestId = crypto.randomUUID(); });
Api.after.each(context => { delete context.requestId; });

Api('responds', async context => {
  const response = await callApi(context.server, context.requestId);
  assert.is(response.status, 200);
});
Api.run();

After and after-each hooks run even after failed assertions. Context is mutable and shared by the suite, so reset per-test state.

Await asynchronous behaviortest-async-code

test('loads a user', async () => {
  const user = await loadUser(7);
  assert.equal(user, { id: 7, name: 'Ada' });
});

test.run();

Return or await the promise inside the async callback. An unhandled rejection fails the test because uvu treats thrown errors as failures.

Assert details of an async rejectionassert-async-error

test('rejects invalid input', async () => {
  try {
    await createUser({});
    assert.unreachable('should have rejected');
  } catch (error) {
    assert.instance(error, Error);
    assert.match(error.message, 'email');
    assert.is(error.code, 'INVALID_INPUT');
  }
});

The docs recommend explicit try/catch for async failures because `assert.throws` is designed for synchronous functions.

Run only selected tests in one suitefocus-test

const Parser = suite('parser');

Parser.only('handles escaped commas', () => {
  assert.equal(parse('a,\,b'), ['a', ',b']);
});
Parser('handles plain values', () => {
  assert.equal(parse('a,b'), ['a', 'b']);
});
Parser.run();

Multiple tests may be marked only. Remove focused tests before committing or the rest of that suite will not execute.

Skip a known unsupported caseskip-test

test.skip('supports Windows named pipes', () => {
  assert.ok(connectNamedPipe());
});

test.run();

A skipped callback does not run. Keep an issue reference in nearby code so a permanent skip does not become invisible debt.

Compare an inline text snapshotcompare-snapshot

test('serializes config', () => {
  const output = JSON.stringify(buildConfig(), null, 2);
  assert.snapshot(output, '{\n  "mode": "test"\n}');
});

uvu compares against the supplied string and does not manage snapshot files or update expected output automatically.

Compare output with a fixture filecompare-fixture

import fs from 'node:fs';

test('renders report', () => {
  const expected = fs.readFileSync('tests/fixtures/report.txt', 'utf8');
  assert.fixture(renderReport(), expected);
});

Fixture assertions include line numbers in diffs. Normalise platform-dependent newlines if the suite runs across operating systems.

Use Node's assertion libraryuse-node-assert

import { test } from 'uvu';
import assert from 'node:assert/strict';

test('returns records', () => {
  assert.deepEqual(listRecords(), [{ id: 1 }]);
});

test.run();

uvu/assert is optional. Any assertion that throws an Error on failure works with the runner.

Run matching files and ignore fixturesconfigure-cli

{
  "scripts": {
    "test": "uvu tests \\.test\\.[cm]?js$ --ignore fixtures --bail"
  }
}

The second positional argument and ignore values are regular expressions, not glob patterns. Shell and JSON escaping both apply inside package.json.

Add coverage with c8collect-coverage

{
  "scripts": {
    "test": "uvu tests",
    "coverage": "c8 --all npm test"
  },
  "devDependencies": {
    "c8": "^10.0.0",
    "uvu": "^0.5.6"
  }
}

Coverage is not built into uvu. Pin a c8 version compatible with the Node versions your project actually supports.

Alternatives

PackageRegistryPick it when
vitestnpmYou want active development, watch mode, mocking, coverage integration, and Vite-aware transforms
tapenpmYou prefer a streaming TAP test API with minimal global machinery and a long compatibility history
mochanpmYou want a mature flexible runner with a broad reporter, hook, loader, and plugin ecosystem