test
Test-Driven Development
Write the test that fails, then the code that makes it pass, then clean up — one behaviour at a time. The test comes first because a test written after the code tests the code you wrote, not the behaviour you wanted; and because a test you've never watched fail proves nothing. TDD is the default for feature work and bug fixes. Skip it only when there's no behaviour to test: config, copy, a rename, a pure-formatting change.
Tests verify behaviour, not implementation
A good test exercises real code paths through the public interface and reads like a specification — "user can check out with a valid cart". It survives refactors because it doesn't know or care how the code is structured inside. A bad test reaches into the internals: mocks collaborators it shouldn't, asserts on private methods, checks which functions were called instead of what came out. The tell: you rename an internal function or restructure a module and tests go red while behaviour is unchanged. That test was testing the implementation. Delete or rewrite it.
Assert on state and outputs, not interactions. Prefer real implementations > fakes > stubs > mocks, in that order — mock only at boundaries that are slow, non-deterministic, or have side effects you can't control (network, email, the clock). Over-mocking buys a suite that's green while production is on fire.
Readable tests beat clever tests
Prefer DAMP tests over DRY tests: descriptive and meaningful phrases are better than a clever helper that hides the behaviour under test. Duplication in test setup is fine when it makes each test read as its own specification. Extract only when the helper's name makes the behaviour clearer, not merely shorter.
The anti-pattern: horizontal slices
Do not write all the tests, then all the code. Treating RED as "write every test" and GREEN as "write every implementation" produces tests for imagined behaviour — you commit to the shape of things before you understand them, and the tests end up insensitive to the changes that matter.