Automated tests check that your code does what you expect — and keep checking every time you change it. Node.js includes a test runner (node:test) and assertions (node:assert/strict), so you don't need extra libraries to start.
Group related tests with describe, write each case with it (or test), and cover the normal case, edge cases (empty input, zero, boundaries) and error cases.
Pure functions — same input, same output, no side effects — are the easiest to test, which is one more reason to keep business logic separate from I/O.
export function gradeFor(score: number): string {
if (!Number.isFinite(score) || score < 0 || score > 100) {
throw new RangeError("Score must be between 0 and 100");
}
if (score >= 75) return "A";
if (score >= 65) return "B";
if (score >= 45) return "C";
if (score >= 30) return "D";
return "F";
}import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { gradeFor } from "./grade";
describe("gradeFor", () => {
it("returns A for top scores", () => {
assert.equal(gradeFor(90), "A");
});
it("handles grade boundaries exactly", () => {
assert.equal(gradeFor(75), "A");
assert.equal(gradeFor(74), "B");
assert.equal(gradeFor(30), "D");
assert.equal(gradeFor(29), "F");
});
it("rejects out-of-range scores", () => {
assert.throws(() => gradeFor(101), RangeError);
assert.throws(() => gradeFor(-1), RangeError);
});
});node --import tsx --test grade.test.tsKey points
- Test behaviour, including boundaries and failure cases — not just the happy path.
node:test+node:assert/strictneed no extra dependencies.- Keep logic in pure functions so it's easy to test.
Exercise
Write tests for the parseAge function from the Error Handling lesson: a valid age, a decimal, a negative number, text input and the boundaries 0 and 120. Add a test script to package.json.
Show solution
Try the exercise yourself first — then compare your approach with this one.
Test the normal case, each failure reason, and both boundaries (0 and 120). Each test checks the whole Result with assert.deepEqual, so a wrong error message is caught too.
export type Result<T> = { ok: true; value: T } | { ok: false; error: string };
export function parseAge(input: string): Result<number> {
const n = Number(input);
if (!Number.isInteger(n)) return { ok: false, error: "Age must be a whole number" };
if (n < 0 || n > 120) return { ok: false, error: "Age must be between 0 and 120" };
return { ok: true, value: n };
}import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { parseAge } from "./parse-age";
describe("parseAge", () => {
it("accepts a valid age", () => {
assert.deepEqual(parseAge("17"), { ok: true, value: 17 });
});
it("accepts the boundaries 0 and 120", () => {
assert.deepEqual(parseAge("0"), { ok: true, value: 0 });
assert.deepEqual(parseAge("120"), { ok: true, value: 120 });
});
it("rejects decimals and text", () => {
assert.deepEqual(parseAge("17.5"), { ok: false, error: "Age must be a whole number" });
assert.deepEqual(parseAge("abc"), { ok: false, error: "Age must be a whole number" });
});
it("rejects ages out of range", () => {
assert.deepEqual(parseAge("-1"), { ok: false, error: "Age must be between 0 and 120" });
assert.deepEqual(parseAge("121"), { ok: false, error: "Age must be between 0 and 120" });
});
});{
"scripts": {
"test": "node --import tsx --test *.test.ts"
}
}