Unit Testing
Date: 2026-08-17
Testing a piece of code in isolation. The word “unit” is doing all the work and nobody agrees what it means — pick the boundary too small and your tests break on every refactor, which teaches people not to refactor.
A unit test exercises a single piece of code independently of its collaborators, verifying behaviour for given inputs.
The unit boundary is the whole decision
TOO SMALL — every function
→ tests coupled to implementation
→ renaming a private helper breaks
tests
→ refactoring means rewriting tests
→ people stop refactoring
ABOUT RIGHT — a module's public
behaviour
→ tests describe what it does
→ internals can change freely
→ tests only fail when behaviour does
TOO LARGE — the whole system
→ not a unit test
→ slow, and failures don't localise
The test to apply: if I restructure the internals without changing behaviour, should this test fail? If yes, the boundary is too small.
This is the single most consequential decision in testing, and it’s usually made by accident — the boundary ends up being whatever’s convenient to import.
Test behaviour, not implementation
// BRITTLE — asserts HOW
expect(calculator._applyVat)
.toHaveBeenCalledWith(50, 0.2)
// DURABLE — asserts WHAT
expect(calculateTotal(50)).toBe(60)The second survives a rewrite of the internals. The first is a test of the current implementation, and its only function is to break when someone improves it.
A useful heuristic: if the test names a private method, it’s testing implementation.
Structure
test('applies VAT to the net price', () => {
// Arrange
const order = { net: 50, vatRate: 0.2 }
// Act
const total = calculateTotal(order)
// Assert
expect(total).toBe(60)
})One behaviour per test. A test asserting six things fails on the first and tells you nothing about the other five.
Name tests as sentences about behaviour:
✓ applies VAT to the net price
✓ returns zero for an empty basket
✓ throws when the VAT rate is negative
✗ test1
✗ calculateTotal works
✗ should work correctly
The test name should tell you what broke without opening the file — because that’s the only thing you see in a CI failure.
What deserves unit tests
Pure logic with rules and edge cases — where they’re genuinely the right tool:
GOOD CANDIDATES
pricing and VAT calculation
discount stacking rules
validation
date and timezone handling
parsing and formatting
state machine transitions
— State Machines
POOR CANDIDATES
glue code with no logic
a component that only renders props
anything whose value is in its
integration
— Integration Testing
See: State Machines · Integration Testing
A function with branching rules and money involved is the ideal unit test. A component that passes props through is better covered by rendering it.
Edge cases are the point
describe('calculateTotal', () => {
test('handles an empty basket')
test('handles a single item')
test('rounds to two decimal places')
test('handles a 100% discount')
test('rejects a negative quantity')
test('handles the maximum quantity')
})The happy path is rarely where bugs are. Zero, one, many, negative, maximum, null, empty string, and the boundary either side of every threshold.
Rounding is the one that costs money. Any monetary calculation should have a test asserting the pennies — Contribution Margin.
Mocking, carefully
// reasonable — an external boundary
vi.mock('./api/fetchPrices')
// suspicious — mocking your own code
vi.mock('./calculateVat')Mocking your own modules usually means the unit boundary is wrong. If a function needs three of your own modules mocked to test, it isn’t isolated — it’s entangled, and the test is documenting the entanglement — Test Doubles, Coupling and Cohesion.
What they can’t do
- Prove the pieces work together. Every unit passing with mocks is compatible with a completely broken application
- Catch contract drift. Your mock returns what the API returned in March
- Test anything about rendering, layout or the browser
Which is why a suite that’s only unit tests gives false confidence, and why the weighting usually belongs at the integration level for web applications — Testing Strategy.