JavaScript · lesson 20 of 24
Beginner
11- 1 Introduction to JavaScript
- 2 Setting Up JavaScript Development Environment
- 3 Variables in JavaScript: var, let, and const
- 4 JavaScript Data Types
- 5 JavaScript Operators
- 6 Control Flow in JavaScript
- 7 Functions in JavaScript
- 8 Working with Strings in JavaScript
- 9 Arrays in JavaScript
- 10 Objects in JavaScript
- 18 DOM Manipulation in JavaScript
Intermediate
10- 11 Error Handling in JavaScript
- 12 Object-Oriented Programming in JavaScript
- 13 Prototypes and Prototype Chain in JavaScript
- 14 Closures in JavaScript
- 15 JavaScript Event Loop Explained
- 16 Asynchronous JavaScript: Callbacks, Promises, and Async/Await
- 17 JavaScript Modules: ESM and CommonJS
- 19 Browser Web APIs in JavaScript
- 20 Testing JavaScript with Jest and Vitest
- 24 JavaScript Interview Preparation: Top 35 Questions
Advanced
3Also part of: Software Testing
Testing JavaScript with Jest and Vitest
Learn how to write reliable unit tests in JavaScript using Jest and Vitest — from basic assertions to mocking, async tests, and code coverage.
By SkillByExample · Updated
A test is a small program that runs your code and checks the result, so a machine — not your memory — notices when behaviour changes. The examples use Vitest, whose API matches Jest’s almost exactly (vi.fn() ↔ jest.fn()). Every output block below is a real npx vitest run in a project where npm install --save-dev vitest has been run.
The testing pyramid is a useful budget: many fast unit tests, fewer integration tests, very few end-to-end tests. This lesson is about the unit-test layer.
describe / it / expect
describe groups tests, it names one behaviour, expect(...) asserts.
// math.js
export function add(a, b) { return a + b; }
export function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// math.test.js
import { describe, it, expect } from "vitest";
import { add, divide } from "./math.js";
describe("add", () => {
it("adds two positive numbers", () => expect(add(2, 3)).toBe(5));
it("handles negative numbers", () => expect(add(-1, -1)).toBe(-2));
});
describe("divide", () => {
it("divides", () => expect(divide(10, 4)).toBe(2.5));
it("throws on division by zero", () => {
expect(() => divide(10, 0)).toThrow("Division by zero");
});
});
$ npx vitest run basics --reporter=verbose
RUN v5.0.3 /root/my-project
✓ basics/math.test.js > add > adds two positive numbers 2ms
✓ basics/math.test.js > add > handles negative numbers 0ms
✓ basics/math.test.js > divide > divides 0ms
✓ basics/math.test.js > divide > throws on division by zero 1ms
Test Files 1 passed (1)
Tests 4 passed (4)
Start at 18:29:40
Duration 211ms (transform 47%, import 37%, tests 9%, worker 7%)
--reporter=verbose lists every test by its describe › it path, which is why test names should read as sentences about behaviour. Note toThrow takes a function — expect(divide(10, 0)) would throw before expect ever ran.
Matchers, and What a Failure Looks Like
Picking the right matcher gives a precise failure message. This file has one deliberately wrong assertion.
// matchers.test.js
import { it, expect } from "vitest";
it("passes with the right matchers", () => {
expect({ a: 1, b: [2] }).toEqual({ a: 1, b: [2] });
expect(0.1 + 0.2).toBeCloseTo(0.3);
expect("hello world").toMatch(/world/);
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect({ id: 1, name: "Alice", role: "admin" }).toMatchObject({ name: "Alice" });
expect(null).toBeNull();
expect(() => JSON.parse("{")).toThrow(SyntaxError);
});
it("fails: toBe on objects compares identity", () => {
expect({ a: 1 }).toBe({ a: 1 });
});
$ npx vitest run matchers
RUN v5.0.3 /root/my-project
❯ matchers/matchers.test.js (2 tests | 1 failed) 13ms
× fails: toBe on objects compares identity 6ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL matchers/matchers.test.js > fails: toBe on objects compares identity
AssertionError: expected { a: 1 } to be { a: 1 } // Object.is equality
If it should pass with deep equality, replace "toBe" with "toStrictEqual"
Expected: { a: 1 }
Received: serializes to the same string
Compared values have no visual difference.
❯ matchers/matchers.test.js:16:20
14|
15| it("fails: toBe on objects compares identity", () => {
16| expect({ a: 1 }).toBe({ a: 1 });
| ^
17| });
18|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
Start at 18:29:41
Duration 231ms (transform 40%, import 25%, tests 24%, worker 11%)
toBe is Object.is — identity for objects — so two separate { a: 1 } objects fail even though they look equal; Vitest even suggests toStrictEqual. Use toBe for primitives and toEqual/toStrictEqual for structures. toBeCloseTo exists because 0.1 + 0.2 is not exactly 0.3. The failure report shows the assertion, a diff and the source line — this is why specific matchers beat expect(x === y).toBe(true), whose failure would only say “expected false to be true”.
Mocking a Module
To test code in isolation, replace its dependencies. vi.mock swaps a whole module for auto-mocked functions.
// service.js
export async function fetchUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
// greeter.js
import { fetchUser } from "./service.js";
export async function greetUser(id) {
const user = await fetchUser(id);
return `Hello, ${user.name}!`;
}
// greeter.test.js
import { describe, it, expect, vi, beforeEach } from "vitest";
import { greetUser } from "./greeter.js";
import { fetchUser } from "./service.js";
vi.mock("./service.js");
beforeEach(() => vi.resetAllMocks());
describe("greetUser", () => {
it("greets the user by name", async () => {
fetchUser.mockResolvedValue({ id: 1, name: "Alice" });
await expect(greetUser(1)).resolves.toBe("Hello, Alice!");
expect(fetchUser).toHaveBeenCalledWith(1);
expect(fetchUser).toHaveBeenCalledTimes(1);
});
it("propagates fetch errors", async () => {
fetchUser.mockRejectedValue(new Error("Network error"));
await expect(greetUser(1)).rejects.toThrow("Network error");
});
});
$ npx vitest run mocking --reporter=verbose
RUN v5.0.3 /root/my-project
✓ mocking/greeter.test.js > greetUser > greets the user by name 4ms
✓ mocking/greeter.test.js > greetUser > propagates fetch errors 2ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 18:29:42
Duration 241ms (transform 56%, import 28%, tests 11%, worker 5%)
No network request was made: vi.mock is hoisted above the imports, so greeter.js received the mocked fetchUser. Each test sets its own behaviour, and vi.resetAllMocks() in beforeEach stops call counts and return values leaking from one test into the next. Mock at boundaries you don’t control — network, filesystem, clock — and let your own code run for real.
vi.fn and vi.spyOn
vi.fn() makes a standalone function that records its calls; vi.spyOn wraps a method on an existing object.
// fns.test.js
import { it, expect, vi } from "vitest";
import fs from "node:fs";
const processItems = (items, keep) => items.filter((item) => keep(item));
const loadConfig = (path) => JSON.parse(fs.readFileSync(path, "utf8"));
it("records every call to a callback", () => {
const keep = vi.fn((item) => item > 2);
expect(processItems([1, 2, 3, 4], keep)).toEqual([3, 4]);
expect(keep).toHaveBeenCalledTimes(4);
expect(keep).toHaveBeenNthCalledWith(1, 1);
expect(keep.mock.results.map((r) => r.value)).toEqual([false, false, true, true]);
});
it("replaces fs.readFileSync for one test", () => {
const spy = vi.spyOn(fs, "readFileSync").mockReturnValue('{ "port": 3000 }');
expect(loadConfig("/etc/app/config.json").port).toBe(3000);
expect(spy).toHaveBeenCalledWith("/etc/app/config.json", "utf8");
spy.mockRestore();
expect(() => loadConfig("/etc/app/config.json")).toThrow("ENOENT");
});
$ npx vitest run fns --reporter=verbose
RUN v5.0.3 /root/my-project
✓ fns/fns.test.js > records every call to a callback 4ms
✓ fns/fns.test.js > replaces fs.readFileSync for one test 1ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 18:29:44
Duration 235ms (import 40%, transform 37%, tests 12%, worker 10%)
keep.mock.results shows what every call returned — handy when a callback misbehaves. The spy test never touched the disk: /etc/app/config.json doesn’t exist, as the last assertion proves once mockRestore() puts the real readFileSync back. Forgetting mockRestore would leave the fake in place for later tests in the same file.
Async Code and Fake Timers
Return or await every promise you assert on. For time-based code, fake timers let a test “wait” 300 ms instantly.
// async.test.js
import { it, expect, vi } from "vitest";
const validateEmail = async (s) => { if (!s.includes("@")) throw new Error("Invalid email"); return s; };
function debounce(fn, delay) {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); };
}
it("awaits resolves/rejects", async () => {
await expect(validateEmail("a@b.co")).resolves.toBe("a@b.co");
await expect(validateEmail("nope")).rejects.toThrow("Invalid email");
});
it("debounce fires once, after the delay", () => {
vi.useFakeTimers();
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced("a"); debounced("b"); debounced("c");
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(299);
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(fn).toHaveBeenCalledExactlyOnceWith("c");
vi.useRealTimers();
});
$ npx vitest run async --reporter=verbose
RUN v5.0.3 /root/my-project
✓ async/async.test.js > awaits resolves/rejects 3ms
✓ async/async.test.js > debounce fires once, after the delay 4ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 18:29:45
Duration 198ms (transform 45%, import 28%, tests 18%, worker 9%)
The fake-timer test checks the boundary exactly: nothing at 299 ms, one call with the last argument at 300 ms — and it ran in milliseconds instead of waiting.
The un-awaited assertion
Forgetting await is the classic async-test bug. Here are two versions of it:
// forgot-await.test.js
import { it, expect } from "vitest";
const saveUser = (name) =>
new Promise((resolve) => setTimeout(() => resolve({ name: name.toUpperCase() }), 50));
it("BUG 1: un-awaited .resolves", () => {
expect(saveUser("bob")).resolves.toEqual({ name: "bob" });
});
it("BUG 2: assertion inside an un-awaited .then", () => {
saveUser("bob").then((user) => expect(user.name).toBe("bob"));
});
$ npx vitest run pitfall --reporter=verbose
RUN v5.0.3 /root/my-project
× pitfall/forgot-await.test.js > BUG 1: un-awaited .resolves 63ms
→ expected { name: 'BOB' } to deeply equal { name: 'bob' }
→ Promise returned by `expect(actual).resolves.toEqual(expected)` was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not guaranteed to complete before the test finishes:
await expect(actual).resolves.toEqual(expected)
✓ pitfall/forgot-await.test.js > BUG 2: assertion inside an un-awaited .then 0ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL pitfall/forgot-await.test.js > BUG 1: un-awaited .resolves
AssertionError: expected { name: 'BOB' } to deeply equal { name: 'bob' }
- Expected
+ Received
{
- "name": "bob",
+ "name": "BOB",
}
❯ pitfall/forgot-await.test.js:8:26
6|
7| it("BUG 1: un-awaited .resolves", () => {
8| expect(saveUser("bob")).resolves.toEqual({ name: "bob" });
| ^
9| });
10|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
FAIL pitfall/forgot-await.test.js > BUG 1: un-awaited .resolves
Error: Promise returned by `expect(actual).resolves.toEqual(expected)` was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not guaranteed to complete before the test finishes:
await expect(actual).resolves.toEqual(expected)
❯ pitfall/forgot-await.test.js:8:26
6|
7| it("BUG 1: un-awaited .resolves", () => {
8| expect(saveUser("bob")).resolves.toEqual({ name: "bob" });
| ^
9| });
10|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
Start at 18:29:46
Duration 347ms (tests 58%, transform 24%, import 12%, worker 6%)
Both assertions are wrong — saveUser upper-cases the name — but only the first test failed. Vitest 5 tracks expect(...).resolves/.rejects promises and fails a test that returns without awaiting one. The second test passed: the test function returned immediately, and by the time the .then callback ran its failing expect, nothing was listening. Any assertion inside a callback must be reached through an await (or a returned promise): const user = await saveUser("bob"); expect(user.name).toBe("bob");.
Setup and Teardown
beforeAll/afterAll run once per describe block; beforeEach/afterEach wrap every test.
// hooks.test.js
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
const log = [];
describe("users table", () => {
let db;
beforeAll(() => { db = new Map(); log.push("open db"); });
afterAll(() => { log.push("close db"); console.log(log.join(" -> ")); });
beforeEach(() => { db.set("seed", { name: "Seed" }); log.push("seed"); });
afterEach(() => { db.clear(); log.push("truncate"); });
it("inserts a user", () => {
db.set("alice", { name: "Alice" });
expect(db.size).toBe(2);
});
it("starts from a clean table", () => {
expect([...db.keys()]).toEqual(["seed"]);
});
});
$ npx vitest run hooks --reporter=verbose
RUN v5.0.3 /root/my-project
stdout | hooks/hooks.test.js > users table
open db -> seed -> truncate -> seed -> truncate -> close db
✓ hooks/hooks.test.js > users table > inserts a user 2ms
✓ hooks/hooks.test.js > users table > starts from a clean table 2ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 18:29:47
Duration 209ms (transform 47%, import 28%, tests 15%, worker 10%, environment 1%)
The log shows the order: one open, then seed → test → truncate around each test, then one close. The second test sees only the seed row even though the first test inserted Alice — that isolation is what lets tests run in any order. (Vitest shows console.log output from tests under a stdout | label.)
Snapshot Tests
A snapshot saves a value’s serialised form on the first run and fails if it later changes.
// card.js
export const renderUserCard = ({ name, role }) =>
`<div class="card">\n <h2>${name}</h2>\n <span class="badge">${role}</span>\n</div>`;
// card.test.js
import { it, expect } from "vitest";
import { renderUserCard } from "./card.js";
it("renders the user card", () => {
expect(renderUserCard({ name: "Alice", role: "admin" })).toMatchSnapshot();
});
$ npx vitest run snap && cat snap/__snapshots__/card.test.js.snap
RUN v5.0.3 /root/my-project
Snapshots 1 written
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 18:29:48
Duration 253ms (transform 51%, import 28%, worker 13%, tests 8%)
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`renders the user card 1`] = `
"<div class="card">
<h2>Alice</h2>
<span class="badge">admin</span>
</div>"
`;
The first run wrote the snapshot file instead of comparing. Commit it with the test. Now change the markup and run again:
$ sed -i 's/class="badge"/class="pill"/' snap/card.js && npx vitest run snap
RUN v5.0.3 /root/my-project
❯ snap/card.test.js (1 test | 1 failed) 11ms
× renders the user card 9ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL snap/card.test.js > renders the user card
Error: Snapshot `renders the user card 1` mismatched
- Expected
+ Received
"<div class="card">
<h2>Alice</h2>
- <span class="badge">admin</span>
+ <span class="pill">admin</span>
</div>"
❯ snap/card.test.js:6:60
4|
5| it("renders the user card", () => {
6| expect(renderUserCard({ name: "Alice", role: "admin" })).toMatchSnap…
| ^
7| });
8|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Snapshots 1 failed
Test Files 1 failed (1)
Tests 1 failed (1)
Start at 18:29:49
Duration 255ms (transform 42%, import 34%, tests 14%, worker 9%)
The diff pinpoints the change. If it was intended, npx vitest run -u rewrites the snapshot. That’s also the weakness: a snapshot says something changed, not whether the change is correct, and it’s tempting to press -u without reading. Keep snapshots small and review their diffs like code.
Coverage
Coverage reports which lines and branches your tests executed. It finds untested code; it can’t tell you the tests are good. (npm install --save-dev @vitest/coverage-v8 first.)
// shipping.js
export function shippingCost(total, country) {
if (total >= 100) return 0;
if (country === "US") return 5;
if (country === "CA") return 8;
return 15;
}
// shipping.test.js
import { it, expect } from "vitest";
import { shippingCost } from "./shipping.js";
it("free over 100", () => expect(shippingCost(150, "US")).toBe(0));
it("US rate", () => expect(shippingCost(20, "US")).toBe(5));
$ npx vitest run cov --coverage --coverage.include=cov/shipping.js
RUN v5.0.3 /root/my-project
Coverage enabled with v8
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 18:29:50
Duration 358ms (transform 43%, worker 32%, import 20%, tests 5%)
% Coverage report from v8
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 57.14 | 50 | 100 | 50 |
shipping.js | 57.14 | 50 | 100 | 50 | 5-6
-------------|---------|----------|---------|---------|-------------------
=============================== Coverage summary ===============================
Statements : 57.14% ( 4/7 )
Branches : 50% ( 3/6 )
Functions : 100% ( 1/1 )
Lines : 50% ( 2/4 )
================================================================================
Two tests, and the report names exactly what they missed: lines 5–6 of the file (counting the // shipping.js comment as line 1) — the Canada rate and the international fallback. Coverage is a map of blind spots, not a score to maximise — a test that calls shippingCost(20, "CA") without an expect would turn those lines green while checking nothing.
Jest vs Vitest
| Jest | Vitest | |
|---|---|---|
| Mock API | jest.fn(), jest.mock() | vi.fn(), vi.mock() |
| ESM | needs configuration | native |
| Config | jest.config.js | reuses vite.config.* |
| Globals | on by default | import from "vitest" (or globals: true) |
The matcher APIs are nearly identical; moving between them is mostly jest. → vi.. Node also ships a zero-dependency runner, node --test with node:assert, which is fine for small libraries.
Common Pitfalls
- Un-awaited async assertions can pass when they should fail — see above.
- Shared state between tests — reset mocks and data in
beforeEach. - Testing implementation details — assert on outputs and side effects, not on private helpers; tests tied to internals break on every refactor.
- Over-mocking — mock the boundary (network, disk, time), not your own modules, or you end up testing the mocks.
Practice
1. Write tests for clamp(n, min, max) covering below, inside and above the range.
// clamp.test.js
import { it, expect } from "vitest";
const clamp = (n, min, max) => Math.min(Math.max(n, min), max);
it.each([
[-5, 0, 10, 0],
[5, 0, 10, 5],
[50, 0, 10, 10],
])("clamp(%i, %i, %i) is %i", (n, min, max, expected) => {
expect(clamp(n, min, max)).toBe(expected);
});
$ npx vitest run p1 --reporter=verbose
RUN v5.0.3 /root/my-project
✓ p1/clamp.test.js > clamp(-5, 0, 10) is 0 3ms
✓ p1/clamp.test.js > clamp(5, 0, 10) is 5 0ms
✓ p1/clamp.test.js > clamp(50, 0, 10) is 10 0ms
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 18:29:51
Duration 216ms (import 49%, transform 35%, tests 8%, worker 7%)
it.each generates one named test per row, so a failure tells you exactly which case broke.
2. Mock Date so a greeting function can be tested at 9 am and 9 pm.
// greet.test.js
import { it, expect, vi, afterEach } from "vitest";
const greeting = () => (new Date().getHours() < 12 ? "Good morning" : "Good evening");
afterEach(() => vi.useRealTimers());
it("morning", () => {
vi.useFakeTimers({ now: new Date(2026, 0, 1, 9, 0) });
expect(greeting()).toBe("Good morning");
});
it("evening", () => {
vi.useFakeTimers({ now: new Date(2026, 0, 1, 21, 0) });
expect(greeting()).toBe("Good evening");
});
$ npx vitest run p2 --reporter=verbose
RUN v5.0.3 /root/my-project
✓ p2/greet.test.js > morning 5ms
✓ p2/greet.test.js > evening 1ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 18:29:52
Duration 248ms (import 43%, transform 40%, tests 10%, worker 7%)
Fake timers also freeze Date, so the clock becomes an input you control instead of a source of flaky tests.
3. Fix BUG 2 from the async section and confirm it now fails, as it should.
// fixed.test.js
import { it, expect } from "vitest";
const saveUser = (name) =>
new Promise((resolve) => setTimeout(() => resolve({ name: name.toUpperCase() }), 50));
it("assertion reached through await", async () => {
const user = await saveUser("bob");
expect(user.name).toBe("bob");
});
$ npx vitest run p3
RUN v5.0.3 /root/my-project
❯ p3/fixed.test.js (1 test | 1 failed) 65ms
× assertion reached through await 63ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL p3/fixed.test.js > assertion reached through await
AssertionError: expected 'BOB' to be 'bob' // Object.is equality
Expected: "bob"
Received: "BOB"
❯ p3/fixed.test.js:8:21
6| it("assertion reached through await", async () => {
7| const user = await saveUser("bob");
8| expect(user.name).toBe("bob");
| ^
9| });
10|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed (1)
Start at 18:29:54
Duration 302ms (tests 48%, import 26%, transform 21%, worker 4%)
With await, the wrong assertion finally fails — proving the test can detect the bug it was written for. Always watch a new test fail once.
4. Add the missing tests so shipping.js reaches 100% coverage.
// shipping.test.js
import { it, expect } from "vitest";
import { shippingCost } from "../cov/shipping.js";
it.each([
[150, "US", 0], [20, "US", 5], [20, "CA", 8], [20, "FR", 15],
])("shippingCost(%i, %s) = %i", (total, country, cost) => {
expect(shippingCost(total, country)).toBe(cost);
});
$ npx vitest run p4 --coverage --coverage.include=cov/shipping.js
RUN v5.0.3 /root/my-project
Coverage enabled with v8
Test Files 1 passed (1)
Tests 4 passed (4)
Start at 18:29:55
Duration 402ms (transform 48%, worker 26%, import 22%, tests 4%)
% Coverage report from v8
No files with missing coverage.
1 file fully covered.
=============================== Coverage summary ===============================
Statements : 100% ( 7/7 )
Branches : 100% ( 6/6 )
Functions : 100% ( 1/1 )
Lines : 100% ( 4/4 )
================================================================================
Every line and branch executed — and, more importantly, every result was asserted.