Testing#
The 2026 default in JavaScript is vitest, Jest-compatible API on top of Vite,
fast watch mode and ESM-native. jest is still the right
choice on projects that already standardised on it; mocha
plus chai is the older xUnit-flavoured alternative; Node’s
own node --test runner (node:test) ships with the
runtime.
For
e2e and browser tests, see playwright. For HTTP mocking,
see msw and nock.
vitest#
$ npm install --save-dev vitest
Specs live alongside source files (foo.test.js) or under
test/. vitest discovers them automatically.
import {describe, it, expect} from "vitest";
import {trim} from "./string-helpers.js";
describe("trim", () => {
it("removes trailing whitespace", () => {
expect(trim("hi ")).toBe("hi");
});
it("leaves clean strings alone", () => {
expect(trim("hi")).toBe("hi");
});
describe("when given null", () => {
it("throws", () => {
expect(() => trim(null)).toThrow(TypeError);
});
});
});
$ npx vitest
✓ trim (3)
✓ removes trailing whitespace
✓ leaves clean strings alone
✓ when given null > throws
Setup and teardown#
beforeEach / afterEach run around every it;
beforeAll / afterAll run once per describe.
describe("Server", () => {
let server;
beforeEach(async () => {
server = await Server.start(8080);
});
afterEach(async () => {
await server.stop();
});
it("answers ping", async () => {
expect(await server.ping()).toBe("pong");
});
});
Expectations#
The fluent expect(value).toX(...) API. Most-used matchers.
Matcher |
Meaning |
|---|---|
|
strict equality ( |
|
deep structural equality |
|
deep + type check (no missing keys) |
|
boolean coercion |
|
|
|
array / string membership |
|
|
|
string matches a regex |
|
calling a function throws |
|
async assertions |
await expect(fetchUser(1)).resolves.toMatchObject({id: 1});
await expect(fetchUser(0)).rejects.toThrow("invalid id");
Mocks and spies#
vi.fn() makes a spy; vi.spyOn(obj, "method") wraps an
existing method; vi.mock("./module") replaces a module
import.
import {vi} from "vitest";
it("logs on failure", async () => {
const log = {warn: vi.fn()};
await runWith(log);
expect(log.warn).toHaveBeenCalledWith("boom");
});
vi.mock("./network.js", () => ({
fetch: vi.fn().mockResolvedValue({ok: true}),
}));
vi.useFakeTimers() advances timers manually; vi.advanceTimersByTime(ms)
runs scheduled callbacks. Useful for testing setTimeout /
setInterval code without waiting.
Snapshots#
.toMatchSnapshot() writes the value to a .snap file on
first run; subsequent runs compare. Update with
vitest --update. Useful for stable HTML / JSON outputs.
it("renders the report", () => {
expect(render(data)).toMatchSnapshot();
});
Coverage#
vitest runs coverage through c8 or v8 providers.
$ npm install --save-dev @vitest/coverage-v8
$ npx vitest --coverage
The report lands in coverage/ (HTML by default; JSON, lcov,
text summary all configurable).
node:test (stdlib)#
Node 18+ ships its own test runner. No dependencies needed.
import {test} from "node:test";
import assert from "node:assert/strict";
test("trim removes whitespace", () => {
assert.equal(trim("hi "), "hi");
});
$ node --test # runs *.test.js
$ node --test --test-name-pattern="trim"
Lightweight; good for small libraries that do not want a test
framework dependency. Lacks the matcher ergonomics of vitest.
Playwright (e2e)#
End-to-end browser tests. The operator scripts real browser behaviour (Chromium, Firefox, WebKit) and asserts on the rendered page.
$ npm install --save-dev @playwright/test
$ npx playwright install
import {test, expect} from "@playwright/test";
test("login lands on dashboard", async ({page}) => {
await page.goto("/login");
await page.fill('input[name="user"]', "op");
await page.fill('input[name="pass"]', "***");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard");
});