Testing Fundamentals
Arrange, act, assert — and testing behaviour instead of implementation.
40 minDifficulty 2/5testing · craftAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
A function works when you tried it manually last Tuesday. Six weeks later someone changes an unrelated file and it silently breaks, and nobody notices until a customer does. A test is a manual check you never have to remember to redo — it runs automatically, forever, every time anything in the codebase changes.
The mental model
A test is a small program that runs your code and checks the result against what you expect — arrange the situation, act by calling the code, assert the outcome. If the assertion fails, the test fails loudly instead of the bug shipping quietly.
How it works
Arrange, act, assert
Nearly every good test has the same three parts: set up the inputs and any needed state (arrange), call the thing being tested (act), then check the output matches expectations (assert). Tests that blur these three into one tangled block are harder to read and harder to fix when they fail.
Test behaviour, not implementation
A test that checks `calculateTotal` calls `applyTax` internally will break the moment you refactor `calculateTotal` to compute tax differently, even if the output is still correct. Test what a function returns for given inputs, not how it gets there — implementation is allowed to change; behaviour is the contract.
A failing test should tell you what broke, precisely
`expect(total).toBe(42)` failing with "expected 42, got 38" tells you exactly what's wrong. A test that just asserts `expect(result).toBeTruthy()` on a complex object tells you almost nothing when it fails — you'll still have to debug from scratch. Specific assertions are what make a test suite actually useful when something breaks.
Tests are a design tool, not just a safety net
Writing a test forces you to call your code the way a consumer would, before you've committed to an interface. Code that's hard to test — needing five mocks and global state setup — is usually revealing a real design problem, not a testing problem.
The mechanism
A test runner discovers test files, executes each test function in isolation, and collects pass/fail results. Inside each test, arrange sets up state, act invokes the function under test, and assert compares the actual result to the expected one using matcher functions that produce a readable diff on failure.
What people get wrong
- 100% code coverage means the code is well tested.
- Coverage measures which lines executed during tests, not whether the assertions actually check anything meaningful — a test that calls a function and asserts nothing gets full coverage and catches zero bugs. Chasing coverage percentage as a target encourages writing tests that execute code without verifying behaviour, which passes the metric while providing none of the actual safety net.
- You should write tests after the feature is done and working.
- Writing the test alongside or before the code forces you to think about the interface and edge cases while you can still cheaply change them, rather than after the design is locked in. Tests bolted on after the fact tend to just confirm whatever the code already does, including its bugs, rather than specifying what it should do.
- A test suite that never fails is a good test suite.
- A test suite that never catches a regression is either testing trivial things or not running against real changes — a healthy suite occasionally fails when someone breaks something, which is the entire point. Teams sometimes treat test failures as embarrassing rather than as the system working correctly, which quietly pushes people toward writing tests too weak to ever fail.
When not to use it
- The code is a one-off script that will be deleted after a single run.
- Skip formal tests — the cost of writing and maintaining them exceeds the value for code with no future changes to protect against.
- You need confidence the whole system works together, not just one function.
- Reach for integration or end-to-end tests, covered in `unit-vs-integration` and `e2e-testing` — unit tests alone can't catch a wiring problem between two correctly-tested pieces.
Terms
- Assertion
- — A statement in a test that checks an actual value against an expected one, failing the test loudly if they don't match.
- Test runner
- — The tool that discovers, executes, and reports the results of a project's test files.
- Flaky test
- — A test that sometimes passes and sometimes fails against the same code, usually due to timing, ordering, or shared state issues rather than a real bug.
- Regression
- — A previously-working behaviour that breaks as a side effect of an unrelated change — the specific thing a good test suite exists to catch.
Can you recall it?
Why is testing behaviour instead of implementation important, and what's a concrete sign that a test is doing the wrong one?