JavaScript: unit tests with node:assert
Write small tests with assertions, including checks that bad input throws the right error.
const assert = require("node:assert/strict");
function percent(text) {
const n = Number(text);
if (text.trim() === "" || !Number.isInteger(n) || n < 0 || n > 100) {
throw new RangeError("invalid percent: " + text);
}
return n;
}
let failures = 0;
function test(name, fn) {
try {
fn();
console.log("PASS " + name);
} catch (error) {
failures++;
console.log("FAIL " + name + ": " + error.message);
}
}
test("parses 42", () => assert.equal(percent("42"), 42));
test("accepts the boundary 100", () => assert.equal(percent("100"), 100));
test("rejects 101", () => assert.throws(() => percent("101"), RangeError));
test("rejects an empty string", () => assert.throws(() => percent("")));
test("reports the bad text", () => assert.throws(() => percent("abc"), { message: "invalid percent: abc" }));
console.log("failures: " + failures);PASS parses 42 PASS accepts the boundary 100 PASS rejects 101 PASS rejects an empty string PASS reports the bad text failures: 0
Explanation
A unit test calls a function with known input and asserts the expected result. Node.js includes the assert module, and node:test is its built-in test runner. The strict mode of assert compares with ===, which avoids the surprises of loose equality.
The test helper below runs each test in try/catch, prints PASS or FAIL, and counts failures. assert.throws checks both that an error is thrown and what kind. Testing the invalid cases matters as much as the valid ones. Note that Number('') is 0, so checking only that Number returns something is not enough for validation.
Explore the dry run
const assert = require("node:assert/strict"); function percent(text) { const n = Number(text); if (text.trim() === "" || !Number.isInteger(n) || n < 0 || n > 100) { throw new RangeError("invalid percent: " + text); } return n;} let failures = 0;function test(name, fn) { try { fn(); console.log("PASS " + name); } catch (error) { failures++; console.log("FAIL " + name + ": " + error.message); }} test("parses 42", () => assert.equal(percent("42"), 42));test("accepts the boundary 100", () => assert.equal(percent("100"), 100));test("rejects 101", () => assert.throws(() => percent("101"), RangeError));test("rejects an empty string", () => assert.throws(() => percent("")));test("reports the bad text", () => assert.throws(() => percent("abc"), { message: "invalid percent: abc" }));console.log("failures: " + failures);Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Reject empty text, non-integers, and values outside 0 to 100. | — | — | |
| A tiny runner: run the test body and report the outcome. | — | — | |
| A normal case. | — | PASS parses 42 | |
| The error type is checked too. | — | PASS parses 42 PASS accepts the boundary 100 PASS rejects 101 | |
| All five checks pass. | failures = 0 | PASS parses 42 PASS accepts the boundary 100 PASS rejects 101 PASS rejects an empty string PASS reports the bad text failures: 0 |
Cost and common mistakes
Each test is O(1) plus the code under test.
- Number('') and Number(' ') are 0, and Number('1e2') is 100. Validate the text itself, not just the converted number.
- A test that throws for the wrong reason can still pass assert.throws if you do not check the error type or message.
- For larger projects, use the built-in node:test runner, which adds test discovery, hooks, and reports.
Try it yourself
Add a test for a negative number.
Reveal answer and reasoning
test("rejects -1", () => assert.throws(() => percent("-1"), RangeError)); -1 is below the allowed minimum of 0.