Warm-up · Activity 1 of 7
Warm-up from lesson I4.1: a function that throws inside try. What does this print?
try {
JSON.parse("{");
console.log("parsed");
} catch (error) {
console.log("caught", error.name);
}// I4.4 · ~36 min · Intermediate
After this lesson you can write tests for your own functions with what Node.js already has, run them with one command, and read a failing report without guessing.
You will be able to
Warm-up · Activity 1 of 7
try {
JSON.parse("{");
console.log("parsed");
} catch (error) {
console.log("caught", error.name);
}Predict · Activity 2 of 7
import assert from "node:assert/strict";
try {
assert.equal({qty: 2}, {qty: 2});
console.log("same");
} catch (error) {
console.log(error.message.split("\n")[0]);
}Practice · Activity 3 of 7
import {____} from "node:test";Practice · Activity 4 of 7
import {test} from "node:test";
import assert from "node:assert/strict";
test("adds", () => {
assert.equal(1 + 1, 2);
});
test("adds cents", () => {
assert.equal(0.1 + 0.2, 0.3);
});
test("lists", () => {
assert.deepEqual([1, 2].map((n) => n * 2), [2, 4]);
});Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
import {test} from "node:test";
import assert from "node:assert/strict";
async function loadUser(id) {
if (id < 1) throw new RangeError("id must be positive");
return {id, name: "Ada"};
}
test("a bad id is rejected", () => {
assert.rejects(loadUser(1), RangeError);
});Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
Instead of the test runner, a small check() function runs each check in try...catch and prints the first line of the error, which is how node --test decides too, but without timings, so the output stays the same every run. Two checks fail on purpose: equal on two arrays, and an expectation that is wrong.
main.js
import assert from "node:assert/strict";
function lineTotal(item) {
if (!Number.isInteger(item.qty) || item.qty < 1) {
throw new RangeError("Quantity must be at least 1");
}
return Math.round(item.price * item.qty * 100) / 100;
}
// test() from node:test does the same: run the function, fail if it throws.
let failed = 0;
function check(name, fn) {
try {
fn();
console.log("✔", name);
} catch (error) {
failed++;
console.log("✖", name);
console.log(" " + error.message.split("\n")[0]);
}
}
check("2 x 2.50 is 5", () => assert.equal(lineTotal({price: 2.5, qty: 2}), 5));
check("3 x 0.10 is rounded to 0.3", () => assert.equal(lineTotal({price: 0.1, qty: 3}), 0.3));
check("a cart maps to its totals", () => {
const cart = [{price: 2.5, qty: 2}, {price: 0.1, qty: 3}];
assert.deepEqual(cart.map(lineTotal), [5, 0.3]);
});
check("a quantity of 0 is refused", () => {
assert.throws(() => lineTotal({price: 1, qty: 0}), RangeError);
});
check("the message names the rule", () => {
assert.throws(() => lineTotal({price: 1, qty: 0}), /at least 1/);
});
check("arrays compared with equal", () => assert.equal([5, 0.3], [5, 0.3]));
check("a wrong expectation", () => {
assert.equal(lineTotal({price: 1.2, qty: 2}), 2.5, "1.2 x 2 should be 2.40");
});
console.log(`${failed} of 7 failed`);
Run it with
node main.jsOutput
✔ 2 x 2.50 is 5
✔ 3 x 0.10 is rounded to 0.3
✔ a cart maps to its totals
✔ a quantity of 0 is refused
✔ the message names the rule
✖ arrays compared with equal
Values have same structure but are not reference-equal:
✖ a wrong expectation
1.2 x 2 should be 2.40
2 of 7 failedExercise 1 of 3
checkOrder(order) guards the start of a checkout with assert: an order needs at least one item, and every quantity must be a whole number of at least 1. When a check fails, the AssertionError should say which rule broke: an order needs at least one item, or every quantity must be at least 1. The starter checks the right things, but its messages are assert’s default text.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Run the tests: what message does the AssertionError have now?
Every assert function takes a message as its last argument.
assert.ok(order.items.length > 0, "an order needs at least one item")
One way to solve it. Yours can look different and still pass the checks.
import assert from "node:assert/strict";
export function checkOrder(order) {
assert.ok(order.items.length > 0, "an order needs at least one item");
for (const item of order.items) {
assert.ok(Number.isInteger(item.qty) && item.qty >= 1, "every quantity must be at least 1");
}
return true;
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
import assert from "node:assert/strict";
export function checkOrder(order) {
assert.ok(order.items.length > 0);
for (const item of order.items) {
assert.ok(Number.isInteger(item.qty) && item.qty >= 1);
}
return true;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {checkOrder} from './main.js';
test('a good order passes', () => {
assert.equal(checkOrder({items: [{qty: 2}]}), true, 'checkOrder should return true');
});
test('an empty order names its rule', () => {
assert.throws(() => checkOrder({items: []}), {name: 'AssertionError', message: 'an order needs at least one item'});
});
test('a quantity of 0 names its rule', () => {
assert.throws(() => checkOrder({items: [{qty: 0}]}), {name: 'AssertionError', message: 'every quantity must be at least 1'});
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 3
assertClose(actual, expected, tolerance = 1e-9) should pass when two numbers differ by at most tolerance, and otherwise throw an AssertionError with the message <actual> is not within <tolerance> of <expected>. The starter uses assert.equal, so assertClose(0.1 + 0.2, 0.3) fails although the difference is tiny.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Run the tests: which one fails, and what does the diff show as actual?
Compare the size of the difference: Math.abs(actual - expected) <= tolerance.
assert.ok(condition, message) throws an AssertionError with your message.
One way to solve it. Yours can look different and still pass the checks.
import assert from "node:assert/strict";
export function assertClose(actual, expected, tolerance = 1e-9) {
assert.ok(
Math.abs(actual - expected) <= tolerance,
`${actual} is not within ${tolerance} of ${expected}`
);
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
import assert from "node:assert/strict";
export function assertClose(actual, expected, tolerance = 1e-9) {
assert.equal(actual, expected);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {assertClose} from './main.js';
test('0.1 + 0.2 is close to 0.3', () => {
assertClose(0.1 + 0.2, 0.3);
});
test('0.5 is not close to 0.3', () => {
assert.throws(() => assertClose(0.5, 0.3), {name: 'AssertionError', message: '0.5 is not within 1e-9 of 0.3'});
});
test('a wider tolerance accepts 2.44 for 2.4', () => {
assertClose(2.44, 2.4, 0.05);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 3 of 3
cartTotal(items) should add price × qty for every line and round the result to cents. The test for a cart of 1.10 × 3 and 0.20 × 1 fails: read the line after + in its report, the value cartTotal really returned. Fix cartTotal, not the test.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Run the tests and read the line after + in the diff.
1.1 × 3 is 3.3000000000000003 in floating point, so the sum is not exactly 3.5.
Round once at the end: Math.round(total * 100) / 100.
One way to solve it. Yours can look different and still pass the checks.
export function cartTotal(items) {
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return Math.round(total * 100) / 100;
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export function cartTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {cartTotal} from './main.js';
test('an empty cart costs 0', () => {
assert.equal(cartTotal([]), 0, 'cartTotal([]) should be 0');
});
test('1.10 x 3 and 0.20 x 1 make 3.50', () => {
assert.equal(cartTotal([{price: 1.1, qty: 3}, {price: 0.2, qty: 1}]), 3.5);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsimport {test} from "node:test";
import assert from "node:assert/strict";
test("the order is copied", () => {
const order = {id: 7, items: ["tea"]};
assert.equal(structuredClone(order), order);
});
What Node.js prints
AssertionError [ERR_ASSERTION]: Values have same structure but are not reference-equal:Why, and the fix
equal compares with Object.is, and a copy is a different object, so it fails even though everything inside is the same. To compare what is inside arrays and objects, use assert.deepEqual(structuredClone(order), order).
import {test} from "node:test";
import assert from "node:assert/strict";
function parseQty(text) {
const qty = Number(text);
if (!Number.isInteger(qty) || qty < 1) throw new RangeError("Quantity must be at least 1");
return qty;
}
test("0 is refused", () => {
assert.throws(parseQty("0"), RangeError);
});
What Node.js prints
RangeError: Quantity must be at least 1Why, and the fix
parseQty("0") runs before assert.throws is even called, so the RangeError escapes and fails the test. assert.throws needs a function it can call and watch: assert.throws(() => parseQty("0"), RangeError).
import {test} from "node:test";
import assert from "node:assert/strict";
async function loadUser(id) {
if (id < 1) throw new RangeError("id must be positive");
return {id, name: "Ada"};
}
test("a bad id is rejected", () => {
assert.rejects(loadUser(1), RangeError);
});
What Node.js prints
AssertionError [ERR_ASSERTION]: Missing expected rejection (RangeError).Why, and the fix
assert.rejects returns a promise. Without await, the test returns at once and is marked as passed; the failed check arrives after the test has ended, as a note about asynchronous activity, and node --test marks the whole file as failed. Make the test async and write await assert.rejects(loadUser(1), RangeError).
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.