What is Test-Driven Development (TDD)? A tester's guide to red-green-refactor

Build confidence with Test-Driven Development.

What is Test-Driven Development (TDD)? A tester's guide to red-green-refactor image

Test-driven development (TDD) is a way of building software where you write a small, failing test first, then write just enough code to pass it, then clean up the code before moving on. It runs on a loop called red-green-refactor. It is a development practice, not a testing phase, and it does not make testers optional: TDD teams still need people who find edge cases, run exploratory testing, and cover the integration work that unit tests never touch.

  • Red: write one small failing test that describes a behaviour you want.
  • Green: write the simplest code that makes the test pass.
  • Refactor: improve the design while every test stays green.
  • Testers on TDD teams: contribute edge cases before the test exists, probe behaviour while it is built, and own exploratory and integration coverage.
  • Biggest misconception: TDD is not "testing after coding," and it is not the same thing as BDD.

What TDD actually is

In most traditional work, you write code first and test it afterwards. Test-driven development flips that order. The test comes first.

A developer starts with a list of behaviours the code should have. They pick one behaviour, turn it into a small automated test, and run it. The test fails, because the behaviour does not exist yet. Then they write the smallest amount of code that makes the test pass. Then they tidy the code without changing what it does, and repeat with the next behaviour.

That ordering is what "test-driven" means. The tests drive the code. Each test acts as an executable specification: instead of a written requirement someone has to interpret, you have a runnable example that either passes or fails.

It also helps to know that TDD started as a design practice, not a QA process. Martin Fowler describes it as a technique developed by Kent Beck in the late 1990s as part of Extreme Programming, and points out that writing the test first forces you to think about the interface before the implementation. That design benefit is a big part of why developers adopt it, and it matters to testers too, because a codebase that is easy to test is a codebase that is easier to test well.

The red-green-refactor cycle, step by step

The heart of TDD is a short loop that repeats for every behaviour. The UK Government Digital Service's engineering guide describes the same loop: turn exactly one behaviour on the list into a runnable test, make all tests pass, then refactor while everything stays green.

Red: write one failing test

Pick one behaviour. Write a single automated test for it. Run the test suite and watch it fail.

The failure matters. If the test fails in a way you did not expect, something is off, maybe in the test setup or in an assumption about the system. A test that passes immediately usually means the test is checking nothing, or the behaviour already exists.

One test at a time is the discipline. It keeps feedback fast and makes failures easy to understand.

Green: make it pass

Write the simplest code that makes the new test pass, plus all the previous tests. "Simplest" is doing real work here. If the test says an order of 100 gets a 90 result, the laziest passing implementation might even hardcode 90. That sounds silly, but it is the point: the next test will force the code to become general.

Keep the loop fast. Unit tests should run in seconds. If a test starts getting slow, that is a signal to deal with, not ignore.

Refactor: improve the design

Now, with tests passing, clean the code up. Remove duplication, give things better names, and shape the code so the next change is easy. The tests are your safety net: refactor, run everything, and if it is still green you know behaviour did not change.

This step is where a lot of the design actually happens, and it is the one people skip. A codebase built test-first but never refactored just accumulates small messy chunks. The tests pass, and the code is still painful.

Then you loop: next behaviour, next test, red again. TDD works because each lap is a tight feedback loop: you find out within seconds whether the system does what the latest test says it should.

A worked example: one full cycle on a discount function

Let's walk one cycle on a small, concrete piece of code. A shop wants a discount rule: orders over 50 get 10% off, everything else pays full price.

Step 1: list the behaviours. Before writing anything, the developer (or the whole team) jots down examples:

A worked example: one full cycle on a discount function: An order of 100 gets 10% off, so it costs 90; An order of 40 pays full price; An order of exactly 50, the boundary; Weird inputs
  • An order of 100 gets 10% off, so it costs 90.
  • An order of 40 pays full price.
  • An order of exactly 50, the boundary. Does it get the discount or not?
  • Weird inputs: zero, negative amounts, non-numeric values.

Notice that this list looks a lot like a tester's equivalence classes and boundaries. In a healthy TDD team, testers help build it.

Step 2: red. The developer writes the first test:

def test_order_of_100_gets_10_percent_off():
    assert discount(100) == 90

It fails immediately, because discount does not exist yet. That is the expected red.

Step 3: green. Write the least code that passes:

def discount(total):
    return total - 10

The test passes. So does nothing else, because there is nothing else. This implementation is obviously wrong for most orders, and the next test will prove it.

Step 4: red again, from the list.

def test_order_of_40_pays_full_price():
    assert discount(40) == 40

Now the hardcoded version fails. The developer is forced to write something real:

def discount(total):
    if total > 50:
        return total * 0.9
    else:
        return total

Both tests pass. Green.

Step 5: the tester's edge case goes red. Here is where a tester earns their seat. The requirement said "orders over 50." What happens at exactly 50? The developer may have assumed one answer, but nobody decided. The tester adds the boundary test:

def test_order_of_exactly_50_pays_full_price():
    assert discount(50) == 50

The current code already returns 50 for an input of 50, so this passes, and now the rule "over 50 means over 50, not over or equal" is pinned down by a test instead of by memory. If the team had decided the opposite, the test would have gone red and forced the conversation. Either way, an ambiguity that would have reached production as a bug report is now a settled, documented decision.

Step 6: refactor. With tests green, the developer tidies up:

def discount(total):
    if total > 50:
        return total * 0.9
    return total

Small change here, but in real code the refactor step removes duplication, splits functions, and keeps the design healthy. The tests never moved. Run them, they are green, the behaviour is intact.

Then the loop continues: zero orders, negative orders, invalid input. One behaviour per lap.

What TDD is not

Three mix-ups come up constantly, so let's clear them.

TDD is not testing after coding. Some teams write tests after the code exists and call it TDD. It is not. The defining feature is the order: a failing test exists before the behaviour it describes does. When tests come last, they can only confirm what the code already does, including its accidental behaviour. Written first, they specify what the code should do.

TDD is not BDD. They are related but different, and people mix them up constantly. TDD works at the unit level: one test, one small piece of code, a red-green-refactor loop driven by a developer. BDD works at the behaviour level: the team agrees concrete examples of how the system should behave, often written in a readable format, and those examples become the shared specification. The Cucumber team's own comparison of the two notes that BDD and TDD are not mutually exclusive, and many teams use both. There is a comparison table further down if you want the differences side by side.

TDD is not a whole test strategy. TDD produces lots of unit tests. Unit tests are the base of the test pyramid, and a pyramid needs more than a base. Integration coverage, end-to-end checks, performance, security, usability, and exploratory testing are all still needed. A codebase with a thousand passing unit tests can still be a disaster for real users.

If you like keeping adjacent practices straight, the MoT glossary also covers spec-driven development, another approach where a specification leads the work.

Why TDD helps testers and teams

The benefits are real, and testers feel several of them directly:

Why TDD helps testers and teams: Fast feedback; An executable specification; Safer refactoring; Fewer basic defects escaping; Better testability
  • Fast feedback. Defects surface in seconds, while the code is fresh in someone's head, instead of days later when context is gone.
  • An executable specification. The tests describe what the code is supposed to do, and they cannot go stale the way a document can, because stale tests fail.
  • Safer refactoring. The suite catches behaviour changes while developers restructure code, which means refactoring happens more often and breaks less.
  • Fewer basic defects escaping. The obvious mistakes get caught at the unit level, before anything reaches a test environment.
  • Better testability. Code written to be test-first tends to be smaller, more decoupled, and easier to automate against.

There is a related mindset benefit. The GDS guide argues that TDD cultivates curiosity and skepticism in a team: asking what this code does with these inputs, and whether it really does what it claims. That is a testing mindset, spreading beyond the testers.

Where testers fit in each phase

So if developers are writing the tests, what is left for you? More than you might expect. Here is the phase-by-phase picture:

Phase What the developer does Where testers contribute
Red Turns one behaviour into a failing test Contributes edge cases, boundaries, and acceptance criteria before the test is written; challenges ambiguous rules like "orders over 50"
Green Writes minimal code to pass Pairs with the developer, observes how the code behaves, and probes cases nobody listed yet
Refactor Improves design while staying green Reviews testability, flags tests coupled to implementation details, and starts shaping exploratory charters for the new behaviour

The pattern across all three phases is the same: testers move the input thinking earlier. Instead of finding edge cases after the code exists, you supply them before. A developer who writes test_order_of_100_gets_10_percent_off on their own will miss the exact-50 case. A developer working with a tester usually will not.

Practitioners who have worked in TDD-heavy teams describe the same shift: the tester's value moves away from executing manual checks and toward supplying the cases, automation skills, and exploratory work that developers tend not to cover themselves. The InfoQ article Testers in TDD teams makes that case from direct experience, and it is a candid read about how the role changes.

What testers do beyond TDD

TDD's unit tests cover a slice of quality. The rest is largely yours, and it is not leftover work:

What testers do beyond TDD: Exploratory testing of emergent behaviour; Integration and end-to-end coverage; Test data and environment stewardship; Regression and CI stewardship
  • Exploratory testing of emergent behaviour. Systems do things nobody specified. Unit tests only check what someone thought to specify. Exploratory testing finds the rest.
  • Integration and end-to-end coverage. Developers rarely write the tests that check whether their component actually works with the payment provider, the database, and the queue at 3am. Someone has to.
  • Test data and environment stewardship. Realistic data, clean fixtures, and reliable environments are unglamorous and essential.
  • Regression and CI stewardship. A test suite that nobody maintains becomes noise. Testers who care for the suite keep it worth trusting.

Skills worth building

If you want to be useful on a TDD team, the practical list is short:

Skills worth building: Read and write tests in at least one programming language; Get comfortable with the command line and running the test suite yourself and 2 more
  • Read and write tests in at least one programming language.
  • Get comfortable with the command line and running the test suite yourself.
  • Understand databases well enough to check persistence without a GUI.
  • Build collaboration habits: contributing examples in refinement, pairing, and giving feedback on test design.

None of this means becoming a full-time developer. It means being able to sit in the same loop and speak the same language. If you want a structured path, the MoT Foundation Certificate in Test Automation covers the automation fundamentals that TDD teams work with every day.

The honest limits of TDD

TDD is a good practice with real costs, and pretending otherwise helps nobody:

  • It needs fast loops. The GDS guide is blunt about this: TDD works best for subsecond unit tests and struggles when feedback is slow, such as feature acceptance testing across multiple components.
  • Some things resist unit testing. GUI flows, code with heavy setup, and lots of external dependencies are awkward to test first. Pushing everything into unit tests produces slow, brittle suites.
  • It does not replace exploratory or system-level testing. Unit tests check specified behaviour. They say nothing about what nobody specified.
  • There is an upfront time cost. Teams starting TDD feel slower at first. The payoff comes later, in fewer escaped defects and safer change.

One trap worth naming: a bug found through exploratory work can become a great TDD candidate. Write the failing test that reproduces it first, then fix it. That habit also makes the fix provable. If you are weighing when to fix what, our piece on fixing bugs as soon as you find them makes the case for short feedback on defects.

Common beginner obstacles

Teams new to TDD hit the same walls. Recognise them early:

  • Writing too many tests at once. Test one behaviour per lap. A big batch of failing tests destroys the tight feedback that makes TDD work.
  • Testing implementation details. If a test breaks whenever the code is restructured but the behaviour is unchanged, the test is coupled to the how instead of the what.
  • Letting the suite get slow. Slow suites stop being run. Treat speed as part of the definition of done.
  • Skipping the refactor. Green is not the finish line. The design cleanup is a real phase, not an optional extra.

TDD vs BDD: the differences that matter

ย  TDD BDD
Granularity Unit level: one small piece of code Behaviour level: examples of system behaviour
Starting artifact A failing unit test Agreed examples, often written in a readable scenario format
Cycle Red, green, refactor Example agreed, then built on a TDD-style inner loop
Typical deliverable A suite of automated unit tests Executable specifications that non-programmers can read
Usual driver The developer The whole team, including product and testing

Two practical takeaways. First, they are not rivals: BDD examples usually get implemented with a TDD-style loop inside them. Second, BDD's real value is the conversation before the example is written, which is exactly where testers tend to shine.

Where to go next

If you want to try this yourself, pick a small function in your team's codebase and walk one red-green-refactor lap on it, edge cases and all. If you write any code at all, even test fixtures, you can do this.

And if you want to build these skills alongside people who test for a living, that is what we built the MoTaverse for. A membership gets you on-demand courses, certifications, events, and a global community of testers learning the same things. Sign up for a MoTaverse membership and make TDD the year you got comfortable inside the loop.

Bug of the MoTaverse profile image
Bug of the MoTaverse
Bug Advocate
They/them

Hello. I'm Bug. You will find me across the MoTaverse and within software products across the whole world.

MoTaverse Team
Comments
Sign in to comment
Subscribe to our newsletter
Explore MoT
Test Forward'27 ยท Software Quality in the age of AI image
Tue, 19 Jan 2027
Register today to guarantee your spot!
MoT Software Testing Essentials Certificate image
Boost your career in software testing with the MoT Software Testing Essentials Certificate. Learn essential skills, from basic testing techniques to advanced risk analysis, crafted by industry experts.
This Week in Quality image
Debrief the week in Quality via a community radio show hosted by Simon Tomes and members of the community
Subscribe to our newsletter