Testing & TDD Essentials
Tests aren't insurance you buy after the fact. Written first, they're the cheapest design tool you have.

TL;DR
Tests aren't insurance bought after the fact; written first, they are the cheapest design tool you have. Test-driven development's red-green-refactor cycle gives design feedback while code is still soft, but testing too much or at the wrong level creates brittle suites. Focus tests on behavior and let the value compound.
On this page
The lie we tell juniors
We tell new developers that tests are how you check your work. Write the code, then write some tests to make sure it does what you think. This is true the way “a seatbelt is how you check your driving” is true. It captures the safety and misses the point entirely.
The developers who get the most out of testing don’t treat it as a verification step bolted onto the end. They treat it as a conversation with the design, conducted while the design is still soft enough to change. The tests come first not out of dogma, but because the most valuable feedback a test gives you arrives before the code exists.
What testing is actually for
Strip away the tooling and a test suite does three jobs, in ascending order of importance.
The obvious one is catching mistakes - the regression you’d otherwise ship twice. Find a bug, write a test that reproduces it, then fix it; now that bug can never quietly return. This alone justifies the effort, and it’s where most teams stop thinking.
The second is granting permission to change. A codebase without tests is a codebase nobody dares touch. Every refactor is a bet, and the rational move is to stop betting - to wrap the scary module in another layer rather than fix it. That’s how systems calcify. A green suite flips the incentive. You can rename, restructure, and rip out whole subsystems, and the suite tells you in seconds whether the observable behaviour still holds. Fearlessness, not green checkmarks, is the real product.
The third job is the one nobody puts on the brochure: tests are design feedback. Code that is painful to test is almost always painful to use. If you can’t construct an object without spinning up a database, the object is entangled with infrastructure it has no business knowing about. If a function needs six mocks to test, it has six responsibilities and should be several functions. The pain you feel writing the test is the design defect announcing itself early, while it’s still cheap to fix. Most developers, feeling that pain, blame the test. The test is usually right.
Why writing the test first changes everything
Here is the move that separates people who do TDD from people who merely have tests. Write the failing test before the code that satisfies it.
It sounds backwards, and the first few times it feels backwards. But consider what you’re forced to do. To write the test, you must decide what you’re building before you build it - the name, the inputs, the return value, the way it reports failure. You become the first consumer of your own API, and you experience its ergonomics before a single line of implementation locks them in. Awkward call site? You feel it immediately, and you fix it for free, because nothing depends on it yet.
The rhythm is small and almost dull. Red: a tiny failing test. Green: the crudest code that passes - hardcode the answer if you have to. Refactor: clean up while the bar stays green. Then repeat with the next small bite. The crudeness in the green step is deliberate. You don’t write the general solution; you let the next failing test drag the generality out of you, one concrete example at a time. Code grows to fit the tests, so there’s no speculative machinery, no “we might need this later” that you never do.
Two things fall out of this loop almost as side effects. Coverage takes care of itself, because no line of production code is ever born without a test demanding it. And refactoring becomes genuinely safe, because the suite catches a mistake the instant you make it, while the change is still fresh in your head and trivial to undo.
The trap of testing too much
None of this means more tests are always better. The most common way to ruin a suite is to over-specify - to mock every collaborator and assert every internal call until the test becomes a mirror of the implementation. Such a test passes when the code is broken and fails when the code is merely rearranged. It actively punishes the refactoring that good tests are supposed to enable.
The discipline is to test behaviour through the public contract, not structure. Assert that the cart totals to thirty-five, not that some private helper was invoked twice. Reach for a mock only at a real boundary - the email you send, the card you charge - where the interaction genuinely is the behaviour you care about. Everywhere else, prefer a real object or a lightweight fake and check the result. And know what not to test at all: the standard library, trivial getters, the framework’s own guarantees, the language itself. Spend your tests where the risk and the logic actually live.
Where the value compounds
The shape of a healthy suite is well known - a broad base of fast unit tests, a thinner band of integration tests, a small cap of end-to-end tests that prove the wires connect. Invert that pyramid into an ice-cream cone of slow, flaky end-to-end checks and the suite gets ignored, which is the same as not having one.
But the shape is downstream of the mindset. Tests written first, kept fast, coupled to behaviour and not structure, become an asset that pays out every single day you work in that code. Each one is a small, permanent subtraction from the fear of changing things. That compounding - not the count, not the coverage percentage - is why the best engineers reach for a test before they reach for the implementation. They’re not checking their work. They’re designing it.
Key takeaways 5
- Tests written first shape design, not just verify it.
- TDD follows red, green, refactor.
- Test behavior, not implementation details.
- Too many brittle tests slow change.
- A trusted test suite makes refactoring safe.
Watch & learn
Frequently asked questions
What is test-driven development (TDD)?
TDD is a practice where you write a failing test for a small piece of behavior, write just enough code to pass it, then refactor, repeating in short cycles.
What is the red-green-refactor cycle?
Red: write a test that fails. Green: write the simplest code that passes. Refactor: improve the code's design while keeping all tests passing.
Does TDD slow development down?
It can feel slower at first, but it often reduces debugging and regressions and makes changes safer, which speeds teams up over time.
Go deeper with the free masterclass
Workshop, PDF handbook and curated resources for “Testing & TDD Essentials”.
Related articles

Intro to Statistics for Analysts
You already work with data every day. Here are the five statistical ideas that will stop the numbers from quietly misleading you.

OT Cybersecurity Essentials
In the plant, a cyber incident is not just lost data -- it is a valve that won't close and a line that won't stop. Here is why OT security belongs to safety engineers as much as to IT.

Clean Code & Practical Software Craftsmanship
You type each line of code once, then you and your team read it for years. Here is why writing for the reader is the highest-leverage habit a developer can build.

Comments
No comments yet. Start the conversation.