Table of Contents
Test-driven development
Test-driven development (TDD) is a practice where you write the test before you write the code. The test fails first — because the code does not exist yet — then you write the minimum code to make it pass, then you clean up. The cycle repeats for every new piece of behaviour.
The three steps are often called red-green-refactor:
- Write a failing test (red — the test suite is broken).
- Write the smallest code that makes the test pass (green).
- Refactor: improve names, remove duplication, clean up structure. All tests must stay green.
Then add the next test and repeat.
Why write the test first
Writing a test before the code forces you to think about the interface before the implementation. You have to decide what the function is called, what it takes as arguments, and what it returns — before you write a single line of the function body. This tends to produce cleaner APIs than starting with implementation details and retrofitting a public interface later.
It also means every line of production code exists to satisfy a test. There is no dead code written “for future use” because no test demanded it yet. This is the practical meaning of the “add nothing else” in step 2.
The problem with test-later
Writing tests after the code is tempting because the code is already there and working — at least locally. The problem is that code written without tests in mind is often hard to test. Functions have too many responsibilities, internal state is not injectable, dependencies are hard-coded. The result is that tests get written for the easy paths and the awkward corners get skipped. Over time the test suite drifts away from covering real behaviour and toward covering whatever was convenient to test.
TDD avoids this by treating testability as a first-class design constraint from the start.
Mocking and over-mocking
Tests frequently need to isolate the unit under test from its dependencies: a database, a network socket, a hardware timer. Replacing these with controlled fakes (mocks, stubs, fakes) is legitimate and often necessary.
Over-mocking is when the test ends up asserting on the internal implementation rather than the observable behaviour. A test that breaks every time you rename a private method is testing the wrong thing. Tests should verify what the code does, not how it does it. The rule of thumb: if a refactor that leaves the behaviour identical breaks the test, the test is over-specified.
When to use TDD
TDD works best for business logic with clear input-output contracts: parsers, calculators, state machines, protocol handlers. It is also effective for fixing bugs: write a test that reproduces the bug, watch it fail, then fix the bug. The test becomes a permanent regression guard.
When TDD is awkward
TDD is harder to apply to code whose primary effect is I/O or rendering: a function that draws a frame to a framebuffer, an interrupt handler that writes to a hardware register, a shader. These can be tested, but the setup cost is high and the tests tend to be brittle. In those cases, testing at a coarser integration level is often more practical.
