What you'll build
A small test suite for a shopping cart, using only what ships with Node: node:test for the runner, node:assert for assertions, and built-in mocks and coverage. There's nothing to install and no config file.
Create an empty folder and add the code below. Recent Node 22 releases run .ts files directly by removing the types, so you don't need a build step either.
1. The code under test
export type Item = { price: number; quantity: number };
export function total(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
export async function checkout(items: Item[], charge: (amount: number) => Promise<string>) {
if (items.length === 0) throw new Error("Cart is empty");
return charge(total(items));
}2. Write the tests
If you've used Jest or Vitest, describe and it will look familiar. assert/strict makes every comparison strict by default.
import assert from "node:assert/strict";
import { describe, it, mock } from "node:test";
import { checkout, total } from "./cart.ts";
describe("total", () => {
it("adds price times quantity", () => {
assert.equal(total([{ price: 10, quantity: 2 }, { price: 5, quantity: 1 }]), 25);
});
it("returns 0 for an empty cart", () => {
assert.equal(total([]), 0);
});
});
describe("checkout", () => {
it("charges the cart total", async () => {
const charge = mock.fn(async (_amount: number) => "receipt_1");
const receipt = await checkout([{ price: 20, quantity: 2 }], charge);
assert.equal(receipt, "receipt_1");
assert.equal(charge.mock.callCount(), 1);
assert.deepEqual(charge.mock.calls[0].arguments, [40]);
});
it("rejects an empty cart", async () => {
await assert.rejects(checkout([], async () => "x"), /Cart is empty/);
});
});3. Run them
--test finds files such as *.test.ts by itself:
node --test# tests 4
# suites 2
# pass 4
# fail 04. Add coverage
node --test --experimental-test-coverage# file | line % | branch % | funcs %
# cart.test.ts | 100.00 | 100.00 | 87.50
# cart.ts | 100.00 | 100.00 | 100.00Should you switch?
| You need | Use |
|---|---|
| Tests for a library, CLI or backend | node:test |
| No dependencies to keep updated | node:test |
| Component tests with a browser-like DOM | Vitest |
| Snapshot-heavy or plugin-heavy suites | Vitest or Jest |
Next steps
- Move one small package over and compare how long the suite takes.
- Add
node --testto CI, where a failing test exits with a non-zero code.