Testing JavaScript
Why Tests Exist
A function that works today can break tomorrow — when requirements change, when a dependency updates, when a colleague refactors the code path it depends on. Tests are the only mechanism that makes that breakage visible immediately rather than in production at 2am.
But tests can lie. A test suite that always passes regardless of what the code does is worse than no tests — it gives false confidence. The discipline is in writing tests that can actually fail.
Start by building a minimal test harness from scratch so you understand exactly what testing frameworks do:
describe groups related tests. it runs one test and catches any thrown assertion error. expect returns an object of matchers — each throws if the condition is not met. Jest, Vitest, and Mocha are this pattern, scaled up.
Test Boundary Conditions, Not the Happy Path
Beginners write one test: the case they already know works. Real tests live at the boundaries — the exact values where behaviour changes:
The buggy version passes only the cases away from boundaries. The boundary tests catch it immediately. This is why boundary testing is non-negotiable — bugs almost always live at the edges.
Testing Pure Functions vs Side-Effect Functions
Pure functions are trivial to test. Functions with side effects require isolation:
Each test creates its own fresh StudentStore. Tests that share state contaminate each other — a passing test at 9am can cause a different test to fail at 9pm simply because of run order. Isolation is not optional.
Mocking — Replacing the World With a Controlled Fake
A unit test should test one unit. If your code calls a database, sends an email, or hits an API, those dependencies must be replaced with fakes — otherwise you are testing the network, not your logic:
The EnrolmentService never touches a real database or sends a real email. The mocks record what they were called with — so tests can verify not just that the function returned the right value, but that it interacted with its dependencies correctly.
Property-Based Testing — Let the Computer Find the Bugs
Unit tests prove that specific inputs produce specific outputs. Property-based testing generates hundreds of random inputs and checks that invariant properties hold for all of them. It finds edge cases you would never think to write manually:
The last property test finds the lexicographic sort bug in seconds — something you might not discover with hand-written tests unless you specifically thought to try [10, 9, 2].
Test-Driven Development — Write the Test First
TDD inverts the workflow: write a failing test, write the minimum code to make it pass, refactor. The test defines what the code should do before the code exists:
Writing tests first forces you to think about the interface before the implementation. The tests are the specification — they answer "what should this do?" before you answer "how should this work?"
Code Coverage — What It Means and What It Doesn't
Coverage measures which lines of your code were executed during tests. 100% coverage does not mean the code is correct — it means every line ran at least once. A test can execute a line without checking its output:
What You Have Learned
Testing is not a phase at the end of development. It is the mechanism that makes confident change possible — the difference between "I hope this refactor didn't break anything" and "I know it didn't."
The key ideas:
- A test harness is
describe+it+expect— the frameworks just add better output and tooling - Test boundary conditions — bugs live at the edges, not in the middle of the happy path
- Each test needs a fresh instance of stateful objects — shared state between tests causes order-dependent failures
- Mocking replaces real dependencies with tracked fakes — tests verify logic, not the network or database
- Property-based testing generates hundreds of random inputs and checks invariant properties — finds bugs you would never think to write tests for
- TDD: test first, implement second, refactor third — tests become the specification
- Coverage is a floor, not a ceiling — 100% coverage with weak assertions is false confidence; strong assertions with 80% coverage is more valuable
In the next lesson: TypeScript Essentials — static types, interfaces, generics, and the type system that catches entire categories of bugs before the code ever runs. Everything you have learned in JavaScript translates directly; TypeScript adds a layer of verification on top.