Assertions and Test Structure
The mechanics beneath every test framework: comparisons that fail loudly, before/after hooks, and organizing suites that scale.
Every test framework โ Jest, Vitest, Mocha โ is sugar over the same core: run code, compare, report. Build the core once and you'll understand all of them.
Assertions: fail loudly, precisely
function assertEqual(actual, expected, msg) {
if (!Object.is(actual, expected)) {
throw new Error(`${msg ?? "assert failed"}: expected ${fmt(expected)}, got ${fmt(actual)}`);
}
}
Design rules learned the hard way:
- The message names the expectation, not the mechanism. "Cart total excludes negative quantities" beats "0 !== -5".
- Deep equality for objects:
Object.iscompares references; you need a structural compare (orJSON.stringifyas a crude shortcut โ order-sensitive, undefined-hostile). assertThrows(fn, ErrorType)must verify the type (and ideally the message) โ "it threw something" is too weak.
Structuring suites
describe("formatPrice", () => {
it("formats whole dollars without cents", () => {
/* ... */
});
it("always shows two decimals for fractional prices", () => {
/* ... */
});
});
describe groups one subject; it states one behavior. Nested describes encode context: describe("with an empty cart"). If a test name needs "and", split it.
Hooks
beforeEachโ fresh state per test (the default choice; independence!)afterEachโ cleanup (restore stubbed globals, clear storage)beforeAllโ expensive shared setup (start a server); use sparingly because it couples tests
The classic bug source: tests sharing mutable state (a module-level array one test appends to). Fresh state per test costs milliseconds and buys reliability.
Edge cases first
For any function, before the happy path, enumerate: empty input (array, string, object), boundaries (0, -1, max), wrong types (null, undefined, non-numbers), and duplicates. Most production bugs live there โ write the test that would have caught yours last time.