Test-Driven Development
A software development practice where developers write automated tests before writing the production code, then write just enough code to pass the tests, and refactor continuously.
What It Is
Test-Driven Development (TDD) is an Extreme Programming (XP) engineering practice that inverts the traditional coding workflow. Instead of writing production code first and adding tests later, you begin with a failing test, write the minimum code to make it pass, and then refactor the result. This tight feedback loop produces cleaner designs, fewer bugs, and a comprehensive safety net of automated tests.
By forcing yourself to articulate expected behaviour before implementation, TDD acts as a design discipline as much as a testing discipline. The resulting test suite becomes executable documentation that guards against regressions and gives you confidence to refactor.
The Red-Green-Refactor Cycle
TDD revolves around a three-step micro-loop that typically lasts only a few minutes:
- Red: Write a small, focused test that describes one piece of behaviour. Run it and watch it fail. The failure must be for the right reason, confirming that the test is actually exercising the code you intend.
- Green: Write the simplest possible production code to make the test pass. Do not worry about elegance or duplication at this stage; correctness is the only goal.
- Refactor: Clean up the implementation and the test. Remove duplication, improve names, simplify structure. The test suite must stay green throughout. If it turns red, undo and try again.
When to Use It
TDD is especially valuable in the following situations:
- New feature development: When building fresh functionality where requirements are understood well enough to write specific examples.
- Bug fixes: Start by writing a test that reproduces the bug. Once it fails (red), fix the bug and watch it pass (green). The test then prevents the bug from returning.
- Complex logic or algorithms: Mathematical, financial, or rule-heavy code benefits from the explicit examples that TDD forces you to create.
- Legacy code with poor coverage: Use characterisation tests to pin down existing behaviour before refactoring or extending.
- Code that changes frequently: If a module is under active development, TDD gives you the confidence to modify it without fear.
TDD may be less productive for throwaway prototypes, pure UI exploration, or spikes where the goal is learning rather than deliverable code.
Key Concepts
Red-Green-Refactor
The canonical TDD rhythm. Red proves the test is meaningful; green proves the behaviour exists; refactor ensures the code stays maintainable. Skipping the refactor step is the most common cause of test-induced design damage.
Test First vs Test After
Writing tests after the fact often leads to tests that are tightly coupled to the implementation, harder to read, and less likely to cover edge cases. Test-first development ensures the test is validating behaviour, not merely mirroring the code.
Unit Tests as Design Tool
Well-written TDD tests reveal how a class or function wants to be used. If a test is awkward to write, the underlying API is probably awkward to use. TDD surfaces design pain early, when it is still cheap to fix.
| Concept | Description | Why It Matters |
|---|---|---|
| F.I.R.S.T. | Fast, Independent, Repeatable, Self-validating, Timely | A mnemonic for qualities that keep tests useful and maintainable |
| Arrange-Act-Assert | Set up context, invoke behaviour, verify outcomes | Creates readable, predictable test structure |
| One reason to fail | Each test should check a single concept | Makes diagnosis faster when something breaks |
Common Pitfalls
Writing Tests After Code
Post-hoc tests tend to confirm what the code already does rather than probe what it should do. They also miss edge cases the developer did not consider during implementation. The discipline of test-first prevents this by design.
Testing Implementation Details
Tests that assert on private methods, internal state, or specific call ordering are brittle. When you refactor, these tests break even though behaviour has not changed. Focus on inputs and outputs, not on how the sausage is made.
Abandoning TDD Under Pressure
Deadlines often trigger the urge to skip tests and just ship. This is precisely when TDD pays for itself: the short-term speed gain is quickly erased by regression bugs, manual retesting, and fear-driven development. Teams that maintain the discipline under pressure emerge with cleaner code and fewer surprises.
The New Zealand Context
New Zealand software teams are typically lean. Many companies operate with small QA headcounts or embedded testers, meaning developers carry a larger share of the quality burden. TDD reduces the manual regression burden by automating confirmation of correctness at the unit level.
Local clients, particularly in government, finance, and primary industries, often demand audit trails and evidence of due diligence. A comprehensive TDD suite serves as executable documentation that demonstrates exactly what the system does and does not do. This can shorten security reviews and compliance sign-offs.
Remote work is common in the New Zealand tech sector. TDD enables asynchronous collaboration: a failing test clearly communicates a requirement across time zones far better than a verbal description in a stand-up recording.
Career Level Guidance
| Level | Focus | Milestones |
|---|---|---|
| Junior | Learn the mechanics of Red-Green-Refactor; write small, readable tests for pure functions | Can TDD a calculator or validation rule; understands Arrange-Act-Assert; runs tests before every commit |
| Senior | Use TDD to drive clean architecture; refactor with confidence; teach the practice to juniors | Can TDD across layers (API, service, repository); mocks only external boundaries; spots test smells instantly |
| Test Lead | Set team standards; review test strategy; integrate TDD into CI/CD and Definition of Done | Defines test coverage policies; mentors team on test design; ensures TDD discipline survives project pressure |
Industry Reality
- Most codebases you inherit have little or no TDD history. Practitioners apply TDD selectively — new modules and bug fixes — rather than retrofitting the entire legacy codebase all at once.
- Strict Red-Green-Refactor discipline is far more common in experienced individual contributors than in whole teams. Many developers write tests concurrently with code and call it "TDD," skipping the design feedback the practice is actually meant to deliver.
- Time pressure is the main reason TDD is abandoned mid-sprint. Experienced practitioners protect the practice by showing managers that regressions and debugging time are dramatically reduced across the quarter, not just the ticket.
- Database, UI, and third-party integration code is routinely excluded from strict TDD because the cost of mocking those boundaries is high. The discipline is applied hardest to business logic, domain models, and utility layers.
- In NZ startups and small consultancies, the developer writing the code often reviews their own tests, so social accountability for TDD quality is low. Senior practitioners compensate by including test quality criteria in pull-request checklists and Definition of Done.
Context guide
How the right level of Test-Driven Development (TDD) effort changes based on team context.
| Context | Priority | Why |
|---|---|---|
| Government agency (Revenue NZ, Benefits NZ, CoverNZ) implementing legislative business rules | Essential | Complex eligibility rules interact unpredictably; TDD converts each rule into executable evidence for compliance sign-off and audit. Regressions in tax or benefit calculations carry legal liability. |
| NZ fintech or bank (Harbour Bank, Pacific Bank, KiwiSaver provider) building payment or calculation logic | Essential | A single off-by-one error in a rounding or interest-rate function can affect thousands of accounts. TDD catches these at the unit level before they compound through integration layers. |
| Small NZ startup with a two- to three-person dev team shipping a SaaS product under pace pressure | High | No dedicated QA means developers carry the full regression burden. TDD automates that burden so the team can iterate without fear. Apply TDD strictly to business logic; relax it on UI scaffolding to maintain velocity. |
| Consultancy delivering a fixed-scope project for a NZ council or utility under a tight delivery timeline | High | Handover to a client IT team means the codebase must be self-explanatory. TDD tests serve as living documentation and protect the client from regression when their team makes post-project changes. |
| Internal tooling or data pipeline where the output is consumed by analysts rather than end users | Medium | TDD on transformation logic is valuable; TDD on data-source connectivity or report formatting adds less value relative to effort. Focus test-first on calculation and aggregation logic, and use integration tests for pipeline wiring. |
| Discovery spike or throwaway prototype built to validate a product hypothesis | Low | The goal is learning, not correct deliverable code. If the spike is discarded (as most should be), the tests go with it. Apply TDD only when the spike transitions into production code — at which point it is no longer a spike. |
Trade-offs
What you gain and what you give up when you adopt Test-Driven Development (TDD).
| Advantage | Disadvantage | Use instead when… |
|---|---|---|
| Design pressure surfaces API awkwardness while code is still cheap to change | Upfront investment slows down the first pass — junior developers feel the overhead most acutely before the payoff is visible | Requirements are entirely unknown and exploration is the goal — spike first, then apply TDD once behaviour is understood |
| Every bug fix becomes permanent regression protection — the test suite grows stronger every time something breaks | Tests that assert on internal state rather than observable behaviour break on every refactor, eroding trust in the suite faster than missing tests | The codebase has deep legacy coupling with no seams — characterisation tests (test-after) are more appropriate until refactoring creates testable units |
| Executable documentation that proves system behaviour to compliance auditors — especially valuable under Privacy Act 2020 or NZISM obligations | Mocking infrastructure for external dependencies (databases, HTTP clients) adds complexity and maintenance overhead that can outweigh the design benefit at integration boundaries | Code is UI layout, generated boilerplate, or configuration — BDD acceptance tests or visual regression tools give more signal for less effort |
| Enables fearless refactoring — a green suite after restructuring confirms that observable behaviour is unchanged regardless of internal changes | Without team-wide adoption and a Definition of Done entry, TDD applied by only one or two developers creates a two-tier codebase where tested modules sit alongside untested ones, reducing the overall safety net | The team is under immediate production incident pressure — restore first using the fastest means available, then backfill with a TDD regression test once the incident is resolved |
Enterprise reality
How TDD changes at 200–300-developer scale in NZ — when a squad's test suite is no longer just their own problem.
- CloudBooks's engineering organisation enforces a shared unit test coverage gate across all squads via a centralised CI pipeline — no PR merges to main without 80% coverage on changed modules. At this scale, TDD stops being a personal discipline and becomes an automated compliance checkpoint; squads that skip the red step get caught by the gate, not by peer review.
- Under the Privacy Act 2020 and NZISM, organisations processing health or government data (HealthNZ, Revenue NZ, Benefits NZ) require evidence that logic handling personal information behaves exactly as documented. A passing TDD suite at commit time provides that audit trail automatically — at 200+ developers this is a board-level risk control, not a developer nicety.
- At multi-squad scale, test run time becomes a coordination bottleneck. Organisations like TechServNZ running 15–20 feature squads in parallel adopt test sharding and parallelised CI workers so the full suite still completes in under 15 minutes — without this, the feedback loop that makes TDD valuable breaks down and squads start merging blind.
- Cross-squad contract testing replaces end-to-end integration tests: each squad publishes a consumer-driven contract (Pact is common), and TDD unit tests are written against the contract rather than a live downstream service. This lets 10+ squads TDD in isolation without a shared staging environment becoming the merge-day bottleneck.
◆ What I would do
Professional judgment — when to adopt Test-Driven Development (TDD), when to adapt it, and what to watch for.
The bottom line: TDD earns its keep in proportion to the cost of being wrong — apply it hardest on logic that carries legislative, financial, or patient-safety consequences, and pragmatically on everything else.
Best Practices
- ✓ Write the failing test first, every time — even under deadline pressure. The discipline is the practice; doing it "later" is not TDD.
- ✓ Keep each test small enough to diagnose instantly. If the failure message doesn't tell you exactly what broke and where, the test is too large.
- ✓ Name tests as sentences:
givenAnOverdrawnAccount_whenInterestIsCalculated_thenPenaltyRateApplies. The test name is the spec; treat it as documentation. - ✓ Refactor the test code with the same care as production code. Duplicated setup, magic numbers, and obscure variable names in tests are technical debt.
- ✓ Mock only at architectural boundaries (HTTP clients, databases, external queues). Never mock the class under test or its close collaborators — this defeats the design signal TDD is meant to give you.
- ✓ Run the full unit test suite in under 90 seconds locally and under 10 minutes in CI. If tests are slow, they will be skipped or silenced — fix the cause, not the symptom.
- ✓ When you hit a bug in production, write the failing test first before touching production code. This converts the bug report into permanent regression protection.
- ✓ Pair TDD with a coverage gate in CI (e.g. 80% line coverage minimum) to create a team baseline, but treat coverage as a floor, not a target — a hundred trivial tests at 100% coverage are worse than sixty meaningful ones at 80%.
Common Misconceptions
❌ Myth: TDD means writing tests for everything, including trivial getters, setters, and UI layouts.
Reality: TDD is a design practice applied to behaviour that has logic worth specifying. Trivial pass-through code, generated boilerplate, and purely declarative UI are not meaningful targets for test-first development. Experienced practitioners apply TDD where the design signal is valuable, not mechanically to every line.
❌ Myth: High test coverage proves TDD was used and that the code is safe to change.
Reality: Coverage measures which lines were executed, not whether the assertions are meaningful. It is entirely possible to have 95% coverage with tests that never actually fail when the code is broken (e.g. assertions are missing or wrong). TDD produces a suite that fails predictably when behaviour changes — coverage alone does not.
❌ Myth: TDD slows you down — writing tests doubles the amount of code you have to write.
Reality: TDD typically reduces total effort over a feature's lifetime. The initial overhead of writing tests is more than recovered by eliminating debugging sessions, reducing regression investigation, and enabling fearless refactoring. Studies and practitioner surveys consistently find that teams practising TDD spend less aggregate time on defect correction than those that do not.
Senior engineer insight
Teams that do TDD well treat the red step as a design review, not a bureaucratic hurdle — they read the failing test aloud and ask "does this actually specify the right behaviour?" before writing a single line of production code. What separates them from struggling teams is that they internalize the cycle so thoroughly that a five-minute gap without running tests feels wrong, like coding with your eyes closed. The specific pattern that works: keep a strict time-box of two minutes per green step — if you cannot make the test pass in two minutes, the test is too large, and you split it rather than write more code.
The most common mistake teams make when adopting TDD is starting with integration or UI tests rather than pure unit tests. Integration tests are slow, flaky, and have too many moving parts to give clean design feedback — teams burn out in the first sprint, conclude "TDD doesn't work," and abandon it. Start with business logic functions that take inputs and return outputs, build confidence there, then extend outward.
From the field
A Wellington-based agile consultancy was rebuilding a payroll compliance module for a New Zealand logistics company and committed to strict TDD from day one of the sprint. The team assumed the hardest part would be wiring the red-green-refactor habit — they were wrong. After two sprints the habit was solid, but a junior developer had been quietly writing tests that asserted on internal private state rather than observable outputs, so when the team refactored the tax-rate lookup strategy, sixty tests went red for no behavioural reason. The team spent a full day untangling test noise from real regressions, and the sprint velocity tanked. They added a pull-request rule: any test that references a private method or internal variable requires a review comment explaining why, and the default answer is "restructure the test." Within a month, test reliability improved dramatically and the next refactor sprint ran with zero false failures. The lesson that travels beyond that team: test quality is a first-class engineering concern, not a box you tick — and brittle tests are more dangerous than missing tests because they train the team to ignore red.
Why teams fail here
- Skipping the red step entirely — developers write tests that they know will pass immediately, producing coverage without design signal; this is especially common in sprint-end scrambles when the pressure is to show green CI, not to drive design.
- Testing implementation rather than behaviour — tests that assert on method call counts, internal state, or private helper invocations couple the suite tightly to the current structure, so every refactor breaks the tests even when nothing visible has changed; this destroys trust in the suite faster than almost anything else.
- Treating TDD as a solo practice in an agile team — when only one or two developers on a five-person team apply TDD, the untested code from the others creates integration seams that undermine the tested modules; TDD requires a team agreement and a Definition of Done entry to stick.
- Letting the test suite grow slow without addressing root cause — once the unit suite takes more than two minutes locally, developers start running tests less frequently, which breaks the short feedback loop that makes TDD valuable; slow tests are a design smell (usually too many real I/O calls hidden inside "unit" tests) and must be fixed, not tolerated.
Key takeaway
TDD done well is not a testing practice — it is a design practice that happens to produce a safety net; the tests are proof that you thought carefully before you coded, not a quality gate you bolt on at the end.
Self-Check
Click each question to reveal the answer.
Q: What are the three steps in the Red-Green-Refactor cycle, and what is the purpose of each step?
A: Red — write a failing test that specifies the desired behaviour, confirming the test is meaningful. Green — write the simplest production code that makes the test pass, focusing only on correctness. Refactor — clean up both test and production code, removing duplication and improving readability while keeping all tests green. Skipping any step undermines the discipline and defeats the design benefits TDD is meant to provide.
Q: Why does TDD say to write the minimum code necessary to make a test pass, rather than immediately writing the full, polished implementation?
A: Writing the minimum necessary keeps each cycle short and tight, ensuring that the test — not the developer's intuition — drives what gets built. It also prevents over-engineering by only implementing behaviour that is actually demanded by a failing test. The refactor step is where you clean up; the green step is purely about correctness. This discipline stops "gold-plating" features that have no test coverage and therefore no verified behaviour.
Q: A bug is discovered in production. How does TDD guide you to fix it correctly?
A: Before touching production code, write a unit test that reproduces the bug and watch it fail (red). Then fix the code and watch the test pass (green). The test now permanently protects against that regression. This approach converts every bug report into automated regression coverage, so the same bug cannot silently re-enter the codebase in a future sprint.
Q: What does F.I.R.S.T. stand for, and why does each property matter for a TDD test suite?
A: Fast — tests must run quickly or developers skip them. Independent — each test must set up its own state so tests can run in any order. Repeatable — tests must produce the same result every time, on every machine. Self-validating — the test must report pass or fail without human inspection. Timely — tests are written at the same time as or before the production code. Together these properties keep the test suite trustworthy and fast enough to run on every code change.
Q: When is TDD a poor fit, and what should you do instead?
A: TDD is less valuable for throwaway spikes and prototypes (where the goal is learning, not deliverable code), pure UI layout (where visual correctness cannot be meaningfully expressed in a unit assertion), and exploratory integration work where requirements are entirely unknown. In these cases, explore first, then apply TDD once behaviour is understood. The key signal is whether you can articulate a specific expected outcome — if you cannot, TDD has no failing test to drive from.
Q: Your team is building a KiwiSaver early-withdrawal eligibility calculator for an Benefits NZ portal. Business rules are complex: multiple criteria interact and the rules change with each Budget. Would you apply TDD here, and how would you structure the approach?
A: Yes — complex rule-heavy business logic with frequent change is exactly where TDD earns its keep. Each eligibility rule becomes an explicit failing test before any code is written, so rule interactions are exposed during development rather than in production. When the rules change after a Budget announcement, the existing test suite immediately shows which behaviours have shifted and which remain correct. Organise tests by rule category (age criteria, financial hardship, terminal illness) so failures are fast to diagnose. The audit trail of tests also supports Benefits NZ's compliance obligations by providing executable evidence of what the system enforces.
Q: What is the key difference between TDD and BDD (Behaviour-Driven Development), and when would you choose one over the other?
A: TDD is a developer-facing engineering practice: tests are written in code by the person implementing the feature, and the primary audience is the development team. BDD is a collaboration and communication practice: scenarios are written in plain language (Given/When/Then) jointly by developers, testers, and business stakeholders, and the primary audience spans the whole team. Choose TDD when the goal is design feedback and unit-level correctness; choose BDD when the goal is shared understanding of business requirements and executable acceptance criteria that non-technical stakeholders can read. On a mature team you typically use both: BDD scenarios define acceptance criteria at the feature level, and TDD unit tests drive the implementation beneath them.
Q: A developer says "We practise TDD — we write all our tests at the end of the sprint before the code goes to QA." What is wrong with this, and how do you respond?
A: Writing tests at the end of the sprint is test-after development, not TDD. The defining characteristic of TDD is that the test is written first and fails before any production code exists. Tests written after implementation tend to mirror what the code already does rather than probe what it should do — edge cases the developer did not think of during coding are typically missed. The design benefit is also completely lost: TDD surfaces awkward APIs while they are still cheap to change; post-hoc tests confirm an API that is already locked in. A polite response is to acknowledge that automated tests are valuable regardless of order, then explain the specific benefits that are only available when tests are written first.
Q: An Revenue NZ project is under deadline pressure and the tech lead suggests skipping TDD for the last two sprints to ship faster. How do you evaluate this decision, and what evidence would you bring to the conversation?
A: Skipping TDD under deadline pressure is a classic short-term/long-term trade-off trap. The immediate gain is slightly faster code writing; the deferred cost is increased debugging time, manual regression effort in future sprints, and a growing fear of refactoring as the codebase becomes untested. In a government system like Revenue NZ's, regressions in tax calculation or payment processing carry compliance and reputational risk that dwarfs any sprint speed gain. The evidence to bring: the team's own defect trend data (how many regression bugs in sprints without TDD vs with), plus the fact that every bug now discovered without a test will require manual investigation next time it recurs. A middle-ground proposal — applying TDD strictly to the highest-risk business logic while relaxing it on scaffolding code — is often a credible compromise that preserves the most value.
How this has changed
The field moved. Here is how Test-Driven Development (TDD) evolved from its origins to current practice.
Kent Beck rediscovers TDD while studying Smalltalk testing frameworks at Apple. Write a failing test, write the minimum code to pass it, refactor — the Red-Green-Refactor cycle is born.
Beck formalises TDD in "Extreme Programming Explained." TDD is positioned as a design technique, not just a testing technique.
JUnit and NUnit make unit testing practical. Beck and Eric Gamma write the foundational JUnit guide. The testing framework ecosystem explodes across all languages.
Outside-in TDD (London school, mockist) and inside-out TDD (Chicago school, classicist) emerge as distinct styles. BDD frameworks implement the outside-in approach with a business-readable layer.
AI coding tools write code faster than tests can be written — making test-first harder to maintain as a practice. The property of TDD most worth preserving is the design pressure it creates and the safety net it builds, not strictly the test-first ordering.