Module 9 · Web Development with JavaScript
Testing JavaScript
Test the page's logic (totals, validation and API handling) with small automated tests, using a tiny test runner you can run in the browser console, and see how real projects do the same with Vitest or Jest.
About 20 minutes
The problem
The payment page now has four pieces of logic: the invoice total, the live balance, validation and the API handling. Each was checked by hand once. The next developer who changes the reference format or the rounding will check by hand again, if they remember. Front-end logic deserves the same automated tests as the server's.
The concept
Test the functions, not the clicks
Because the logic lives in plain functions (lessons 6 to 8), most of it can be tested without a browser page at all: call the function, compare the result.
A test is still just an assertion
Every test framework does the same thing: run named checks, report which passed and which failed.
Real projects
In a project with Node.js, use a test framework: Vitest or Jest (npm install -D vitest, then npx vitest). They find *.test.js files, run them on every change, and can simulate a page for DOM tests. For checking whole pages in real browsers, teams use Playwright or Cypress. The tests you write here move into those files almost unchanged.
Example
A test runner small enough to read, then tests for the functions from lessons 2 and 7. Paste the functions into the console first (they're repeated here so this block runs on its own):
JavaScript
const results = [];
function test(name, fn) {
try {
fn();
results.push(["PASS", name]);
} catch (error) {
results.push(["FAIL", `${name}: ${error.message}`]);
}
}
function expectEqual(actual, expected) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
}
const roundHalfUp = (n) => Math.sign(n) * Math.round(Math.abs(n));
function invoiceTotal(lines, discountPct = 0, vatExempt = false) {
if (discountPct < 0 || discountPct > 20) throw new RangeError("discountPct must be between 0 and 20");
const subtotal = lines.reduce((sum, [q, p]) => sum + q * p, 0);
const afterDiscount = subtotal - roundHalfUp((subtotal * discountPct) / 100);
return afterDiscount + (vatExempt ? 0 : roundHalfUp((afterDiscount * 75) / 1000));
}
function validateReference(reference) {
const ref = reference.trim();
if (ref === "") return "Enter the reference from your bank transfer";
if (!/^[A-Za-z0-9-]{6,30}$/.test(ref)) return "References are 6 to 30 letters, numbers or dashes";
return null;
}
test("single line with VAT", () => expectEqual(invoiceTotal([[2, 100000]]), 215000));
test("VAT-exempt customer pays no VAT", () => expectEqual(invoiceTotal([[2, 100000]], 0, true), 200000));
test("VAT half a kobo rounds up", () => expectEqual(invoiceTotal([[1, 220]]), 237));
test("discount above 20% is refused", () => {
let threw = false;
try { invoiceTotal([[1, 100]], 25); } catch { threw = true; }
expectEqual(threw, true);
});
test("empty reference has a message", () => expectEqual(validateReference(" "), "Enter the reference from your bank transfer"));
test("valid reference passes", () => expectEqual(validateReference("TRF-2026-0915-0042"), null));
test("reference of 5 characters is too short", () => expectEqual(validateReference("AB-12"), null));
console.log(results.map(([status, name]) => `${status} ${name}`).join("\n"));
console.log(`${results.filter(([s]) => s === "PASS").length} passed, ${results.filter(([s]) => s === "FAIL").length} failed`);PASS single line with VAT
PASS VAT-exempt customer pays no VAT
PASS VAT half a kobo rounds up
PASS discount above 20% is refused
PASS empty reference has a message
PASS valid reference passes
FAIL reference of 5 characters is too short: expected null, got "References are 6 to 30 letters, numbers or dashes"
6 passed, 1 failedOne test fails, and the failure is in the test, not the code: a 5-character reference is too short (the rule says 6 to 30), so validateReference correctly returns a message. The test's expectation was wrong. Reading a failure carefully, and deciding whether the code or the test is wrong, is half of testing. The fix is to expect the message.
Walkthrough
- Run the block. Fix the failing test's expected value and run it again.
- Add boundary tests for references of exactly 6 and exactly 30 characters, and of 31.
- Write tests for
sendPaymentfrom lesson 8 usingfakeFetch(they'll needawait). - Turn one of these into a Vitest test file (the task below).
Practice
Practice
How many of the seven tests pass in the example?
Task
6 minWrite a Vitest test file for validateReference (imported from ./validate.js), with a describe block and at least three tests: an empty reference, a valid one, and one that's too long (31 characters).
Your work is checked for
- Imports from vitest
- Imports validateReference
- A describe block
- At least three tests
- Uses expect
- A 31-character case
Check your understanding
Answer every question to check.