uvu review
uvu 0.5.6 is a small JavaScript test runner built around executable test files. A file registers cases on the default test suite or a named suite, attaches hooks if needed, and calls run(). The CLI finds files and loads their suites; running the file with Node isolates it. Assertions live in an optional submodule and ordinary thrown errors also fail tests. The current release fixed false passes around process.exit and unresolved promises, corrected BigInt diff output, and made programmatic runner imports possible.
uvu 0.5.6 installed in 1.2 seconds, occupied 1 MB, and produced a 5.5 KB gzipped broad bundle in our sandbox with 0 audit findings. It still suits compact Node test suites, but new projects needing watch, coverage, mocks, or transforms will assemble less plumbing with Vitest.
We installed it
| Install | ✓ · 1.2s | 6 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 5.5 KB | gzipped (12.9 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does uvu install cleanly?
Yes. In a fresh container with an empty cache, npm install uvu finished in 1 seconds, leaving 6 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does uvu add to a browser bundle?
5.5 KB gzipped (12.9 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does uvu work with both ESM and CommonJS?
Yes. Both import 'uvu' and require('uvu') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does uvu include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
uvu or vitest: which should you use?
vitest: Choose it for active development, watch mode, Vite transforms, mocking, fake timers, coverage, and browser options in one runner. uvu 0.5.6 installed in 1.2 seconds, occupied 1 MB, and produced a 5.5 KB gzipped broad bundle in our sandbox with 0 audit findings.
When should you not use uvu?
You expect built-in watch mode; the project documentation delegates watching to an external file watcher
Use it if
- A Node library needs fast, explicit tests with suites, hooks, async support, and few moving parts
- You want test files that also run directly with Node for process-level isolation
- Built-in deep, snapshot, match, throws, and type assertions cover the project's needs
- CommonJS and ESM entry points plus bundled declarations must coexist with Node 8 compatibility
- You expect built-in watch mode; the project documentation delegates watching to an external file watcher
- You need integrated coverage, mocks, fake timers, DOM environments, or transforms; uvu leaves those to separate tools
- Your team wants active framework releases; 0.5.6 was published in July 2022 and the repository's last push was August 2024
- You rely on shell globs for discovery; the uvu CLI treats its filename and ignore patterns as regular expressions
- You need snapshot files with an update workflow; uvu's snapshot assertion compares against an expected value supplied in code
Setup reality
Our uvu 0.5.6 install finished in 1.2 seconds and left 6 packages using 1 MB on disk. It has 4 direct dependencies, 0 peer dependencies, and 124 KB unpacked. npm audit found 0 known vulnerabilities. The package declares Node >=8.
A test file must call test.run() or suite.run() after registration. Forgetting that call can leave a file that loads without executing its cases. Add uvu tests to package.json for collection, or run a single file with Node when isolation matters. The CLI's directory pattern and ignore flags are regular expressions, not shell globs.
TypeScript declarations ship with the package, and the exports map has separate require and import targets. Both forms worked in our Node 22 sandbox. Discovery recognizes TypeScript filename extensions but does not transpile them. Compile first or preload a runtime loader. Version 0.5.6 improved programmatic parse and run imports, although its release note still describes that interface as an area intended for simplification.
Our broad browser bundle measured 12.9 KB minified and 5.5 KB gzipped. The assertions are browser-compatible, but uvu does not supply a browser, DOM, or automation layer. Add c8 or nyc for coverage and an external watcher for reruns. Shared suite context is mutable, so reset it in hooks. For rejected promises, await the call inside a test; an assertion intended for synchronous throws does not replace async rejection handling.
Patterns
Run a basic assertion write-basic-test
import {test} from 'uvu';
import * as assert from 'uvu/assert';
test('adds numbers', () => {
assert.is(2 + 2, 4);
});
test.run();The final test.run() is required; registering the case alone does not execute it when the file loads.
Group cases in a named suite create-named-suite
import {suite} from 'uvu';
import * as assert from 'uvu/assert';
const math = suite('math');
math('sqrt', () => assert.is(Math.sqrt(9), 3));
math.run();A named suite reports its label and maintains its own hooks and mutable context.
Await an asynchronous result test-async-code
test('loads user', async () => {
const user = await loadUser('42');
assert.is(user.id, '42');
});Return or await the promise so 0.5.6 can observe completion and rejection before the process exits.
Create and clean suite context share-test-context
const api = suite('api');
api.before.each((context) => { context.server = startServer(); });
api.after.each(async (context) => { await context.server.close(); });Suite context is mutable and shared through hooks, so each test should receive freshly assigned state when isolation matters.
Compare nested values assert-deep-equality
import * as assert from 'uvu/assert';
assert.equal(
{user: {id: 7, roles: ['editor']}},
{user: {id: 7, roles: ['editor']}}
);assert.equal performs a deep comparison; assert.is checks strict identity and is the better signal for primitives or references.
Check a synchronous exception assert-synchronous-throw
assert.throws(
() => JSON.parse('{'),
/Unexpected end/
);assert.throws expects the callback to throw synchronously; use try and catch around an awaited promise for an async rejection.
Compare an inline snapshot snapshot-inline-value
const output = JSON.stringify({ok: true});
assert.snapshot(output, '{"ok":true}');uvu compares the supplied expected value and does not maintain or update a separate snapshot file.
Isolate one test file with Node run-single-file
node tests/parser.test.mjsThe file still needs its own run() call; direct Node execution avoids loading every suite found by the CLI collector.
Collect files with a regular expression filter-test-files
uvu tests '\.unit\.(js|mjs)$'The second CLI argument is a case-insensitive regular expression, not a glob such as **/*.unit.js.
Use Node's assertion module use-node-assert
import {test} from 'uvu';
import assert from 'node:assert/strict';
test('parses', () => {
assert.deepEqual(parse('a=1'), {a: '1'});
});
test.run();uvu treats thrown errors as failures, so its optional assert package is not required for a 0.5.6 suite.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| vitest | npm | Choose it for active development, watch mode, Vite transforms, mocking, fake timers, coverage, and browser options in one runner. |
| tape | npm | Choose it for TAP output and a streaming assertion style with a long Node and browser history. |
| mocha | npm | Choose it for a mature runner with many reporters, loaders, interfaces, hooks, and third-party integrations. |
More testing guides
pytest · chai · jsdom · vitest · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

