World’s largest virtual agentic engineering & quality conference
Learn what test-driven development is, how the red-green-refactor cycle works, when TDD is worth using, plus a worked example and best practices that hold up.

Piyusha Podutwar
Author

Himanshu Sheth
Reviewer
Published on: September 26, 2025
Last Updated on: August 5, 2026
On This Page
Test driven development inverts the usual order of work: the test comes first, and the production code exists only to satisfy it.
Most suites are written after the code they check. That is why so many of them pass on the first run without ever proving anything.
This guide walks the full red, green, refactor loop with worked examples in JavaScript and Python, and an honest account of where TDD stops paying off.
Overview
Is TDD About Testing or About Design
Both, but design is the part teams underrate. Writing the test first forces you to state expected behaviour before you are attached to an implementation.
What Do You Need Before Starting TDD
Four things have to be in place before the cycle pays for itself.
Test-driven development is a coding discipline: you write a failing test before the production code that satisfies it, then refactor the design once that test passes and stays green.
Kent Beck formalised the practice as part of Extreme Programming in the late 1990s, then documented it in Test Driven Development: By Example in 2002.
The ordering is the whole point. A test written after the code tends to confirm whatever the code already does, including its bugs.
Written first, the test is a specification you have to satisfy. That distinction is why practitioners treat TDD as a design technique that produces tests, rather than a testing technique.
One clarification worth making early, because it causes most of the confusion: TDD operates at the unit level inside the software development life cycle.
It does not replace integration testing, acceptance testing, or regression testing. Those cover the gaps between units that TDD cannot reach.
Teams adopt TDD because it moves defect discovery out of production and back into the edit cycle, and because code written to satisfy a test upfront tends to be far easier to change later.
The strongest evidence is not a vendor claim. Nagappan, Maximilien, Bhat, and Williams studied three teams at Microsoft and one at IBM that adopted TDD.
Their study of test driven development across four industrial teams found pre-release defect density fell between 40% and 90% against comparable projects.
Report the cost alongside the benefit. The same teams reported initial development time rising 15% to 35%. TDD trades early speed for later change safety, and that trade is not always worth making.
Five benefits come up consistently in teams that have run the practice for more than a quarter.
Note what is absent from that list: TDD does not guarantee correctness. It guarantees that the behaviour you thought to test is the behaviour you got.
Note: Run your TDD suites across 10,000+ real browsers and devices. Try TestMu AI Now!
Red, green, refactor is the core loop: write one failing test, write the least code that passes it, then improve the design while the suite stays green. Repeat in minutes, not in hours.

The diagram above shows three phases. At the keyboard the loop is really five steps, and the two that teams skip are steps 2 and 4.
Each step below is one action you take at the keyboard, in order, before starting the next.
Then repeat for the next behaviour. If a cycle is taking an afternoon, the step you chose was too big.
The cycles I regret are the ones where I skipped ahead and wrote three tests before running any of them.
Robert C. Martin compressed the discipline into three constraints. They are deliberately strict, and reading them as absolutes is the fastest way to understand the mechanic.
First law: You are not allowed to write any production code unless it is to make a failing unit test pass.
Second law: You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
Third law: You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
The second law catches people out. A compilation error counts as a failing test, so referencing a class that does not exist yet is a legitimate red state.
Follow the three laws and a side effect appears: everything becomes testable in isolation, which means everything becomes decoupled. TDD pushes you toward loose coupling whether you were aiming for it or not.
Before the realistic example, here is the loop at its smallest. Start with a test for a function that does not exist yet.
// CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@Test
void addsTwoNumbers() {
assertEquals(3, Calculator.addTwoNumbers(1, 2));
}
}This does not even compile, because the Calculator class does not exist. Under the second law, a compilation failure counts as red. Now write the least code that turns it green.
// Calculator.java
class Calculator {
static int addTwoNumbers(int a, int b) {
return a + b;
}
}The test passes. Refactor is now an opportunity, not an obligation: a single return statement has no duplication to remove, and changing working code to fill the step is how bugs get in.
This example makes the mechanics clear but hides the value. The next section works a realistic problem where each cycle actually shapes the design.
Adding two numbers shows the mechanics. This example shows the point.
We will build a password validator in JavaScript with Jest, adding one rule at a time and letting each failing test drive the next piece of code.
The requirements: a valid password is at least 8 characters, contains at least one number, and contains at least one uppercase letter.
We do not implement all three at once. Each rule gets its own cycle, and that constraint is what produces the design later on.
Write the test first, before any implementation exists:
// passwordValidator.test.js
const validate = require('./passwordValidator');
test('rejects a password shorter than 8 characters', () => {
expect(validate('Abc123')).toBe(false);
});
test('accepts a password of at least 8 characters', () => {
expect(validate('Abcd1234')).toBe(true);
});Run npx jest and it fails immediately, because the module does not exist yet.
FAIL ./passwordValidator.test.js
● Test suite failed to run
Cannot find module './passwordValidator' from 'passwordValidator.test.js'That failure matters. It proves the test is capable of failing, and a test that has never failed is not evidence of anything.
Now write the minimum code to pass (Green):
// passwordValidator.js
function validate(password) {
return password.length >= 8;
}
module.exports = validate;Both tests pass. Note how little we wrote. No number check, no uppercase check, nothing we were not asked for yet.
Resisting that urge is the discipline, and it is the part that still feels wrong years in.
Back to Red. Add the next failing test:
test('rejects a password with no number', () => {
expect(validate('Abcdefgh')).toBe(false);
});This fails, because our validator only counts characters. Extend it just enough to pass:
function validate(password) {
if (password.length < 8) return false;
if (!/[0-9]/.test(password)) return false;
return true;
}Same loop again, one rule at a time:
test('rejects a password with no uppercase letter', () => {
expect(validate('abcd1234')).toBe(false);
});Fails as expected. Make it pass:
function validate(password) {
if (password.length < 8) return false;
if (!/[0-9]/.test(password)) return false;
if (!/[A-Z]/.test(password)) return false;
return true;
}Here the refactor step finally earns its place. A real smell has appeared: three near-identical guard clauses, and a fourth rule would mean a fourth.
Because four tests already pass, we have a safety net that makes restructuring safe. Turn the rules into data:
const rules = [
{ name: 'at least 8 characters', test: (p) => p.length >= 8 },
{ name: 'contains a number', test: (p) => /[0-9]/.test(p) },
{ name: 'contains an uppercase letter', test: (p) => /[A-Z]/.test(p) },
];
function validate(password) {
return rules.every((rule) => rule.test(password));
}
module.exports = validate;Run the suite again. All four tests still pass, which is the only reason a change of that size is safe to make.
PASS ./passwordValidator.test.js
✓ rejects a password shorter than 8 characters
✓ accepts a password of at least 8 characters
✓ rejects a password with no number
✓ rejects a password with no uppercase letter
Tests: 4 passed, 4 totalAdding a fourth rule is now one line in an array rather than another guard clause, and the tests double as a readable specification of a valid password.
Notice that nobody designed that rules array up front. It emerged because the third cycle made the duplication obvious and the tests made removing it safe.
The discipline is language agnostic. Here is the same progression in Python, where pytest needs no assertion library and a bare assert is enough.
# test_password_validator.py
from password_validator import validate
def test_rejects_password_shorter_than_8_characters():
assert validate("Abc123") is False
def test_accepts_password_of_at_least_8_characters():
assert validate("Abcd1234") is True
def test_rejects_password_with_no_number():
assert validate("Abcdefgh") is False
def test_rejects_password_with_no_uppercase_letter():
assert validate("abcd1234") is FalseRun pytest and collection fails outright with a ModuleNotFoundError, because password_validator.py does not exist yet. That is still a legitimate red state.
After the same three cycles, the refactored implementation lands on the identical rules-as-data shape:
# password_validator.py
import re
RULES = [
("at least 8 characters", lambda p: len(p) >= 8),
("contains a number", lambda p: bool(re.search(r"[0-9]", p))),
("contains an uppercase letter", lambda p: bool(re.search(r"[A-Z]", p))),
]
def validate(password):
return all(rule(password) for _, rule in RULES)$ pytest -q
.... [100%]
4 passed in 0.02sTwo languages, one loop. If you are choosing a runner, the pytest tutorial covers fixtures and parametrisation, which is where Python TDD gets its leverage.
TDD stops paying off when the risk sits outside the unit boundary, when the correct behaviour is not yet known, or when the code under test is mostly wiring rather than real logic.
Most of the fatigue around TDD comes from teaching it as an all or nothing mandate.
Teams that dutifully unit-tested every class ended up with suites so coupled to implementation that any refactor turned the board red.
That experience taught a generation that TDD means maintenance pain. What it actually demonstrated is that mocking everything you own has a cost.
The 2014 exchange between Kent Beck, Martin Fowler, and David Heinemeier Hansson, and recorded by Thoughtworks as Is TDD dead, remains the most useful summary of that disagreement.
The useful question is not whether to do TDD. It is which code in front of you earns the cycle.
Four conditions make the extra cycle time worth paying, and they tend to appear together.
In these four situations the cycle adds cost without buying much protection.
For the second list, reach for contract and integration tests at the service boundary instead. Those catch what unit level TDD structurally cannot.
The practitioner position: use TDD where logic is intricate and being wrong is costly, and stop defending it everywhere else. A team that applies it selectively keeps the benefit and loses the fatigue.
Ask two experienced practitioners how to do TDD and you may get two different answers, because the community split into two schools.
Both follow red, green, refactor. They disagree on what a unit is, and on how much you should use mock objects.
The Detroit school is the original style, associated with Kent Beck and the Chrysler project where Extreme Programming was born.
It works inside-out: start with the domain objects at the core, get them right, then build outwards toward the user interface.
Its defining trait is state verification. A test calls the code and asserts on the result, exactly as the password validator above does.
Classicists avoid mocks wherever real objects will do, reserving them for awkward collaborators such as a network call or a database.
A unit here is a behaviour, not necessarily a single class, so one test may exercise several real objects together.
The upside: tests couple to behaviour rather than implementation, so you can restructure internals freely and the tests keep passing.
The downside: when a test fails, several real objects were involved, so the failure points at a neighbourhood rather than a line.
The London school grew out of the London Extreme Programming community and works outside-in, starting at the outermost entry point.
When the object under test needs a collaborator that does not exist yet, you replace it with a mock and let that mock define the interface you wish you had.
You then move inwards and implement it for real. Its defining trait is behaviour verification.
Instead of asserting on returned state, you assert that the object under test called its collaborators correctly.
Isolation is close to total: each class is tested with every dependency mocked, so a failure points at exactly one class.
The upside: precise failure localisation, and pressure toward clean interfaces, since designing the mock forces you to design the contract.
The downside: tests know how the code works, not just what it does, so changing collaborators breaks tests even when behaviour never changed.
| Parameters | Classicist (Detroit) | Mockist (London) |
|---|---|---|
| Direction of work | Inside-out, domain first | Outside-in, entry point first |
| What the test asserts | State verification, the result | Behaviour verification, the interactions |
| Use of mock objects | Sparing, mainly awkward collaborators | Heavy, most dependencies mocked |
| What a unit means | A behaviour, possibly several classes | A single class in isolation |
| Coupled to | Behaviour, so refactors are safer | Implementation, so refactors can break tests |
| When a test fails | Points at a neighbourhood | Points at one class |
In practice most teams are not purists. Work classicist by default, because behaviour-coupled tests survive refactoring.
Reach for mocks at the boundaries, where collaborators are slow, non-deterministic, or carry side effects you cannot afford, such as payment gateways or email.
If you find yourself mocking objects you own and control, that is usually a design signal rather than a testing requirement.
Every suite I have had to rescue was mockist by accident rather than by decision.
The password validator above shows the mechanics on one small problem. Here is where teams apply the same cycle across different industries:
The difference is ordering, and ordering changes what the test is for. TDD is test-first, so the test is a specification you have not satisfied yet rather than a check on code that already exists.
Traditional testing is test-last, so the test is a check on code that already exists. That check inherits whatever assumptions the code made, including the wrong ones.
| Parameters | Test-driven development | Traditional testing |
|---|---|---|
| Order of work | Test first, then production code | Production code first, then tests |
| What the test is | A specification to satisfy | A check on existing behaviour |
| Who writes it | The developer, during implementation | Often a separate QA function, afterwards |
| Feedback latency | Seconds, inside the edit cycle | Hours to days, after a build |
| Effect on design | Shapes the design as it emerges | Observes the design after the fact |
| Proven able to fail | Yes, the red step forces it | Not necessarily, may pass on first run |
| Typical blind spot | Integration gaps between units | Assumptions baked into the code |
One caveat worth stating plainly: the widely repeated claim that TDD yields 90% to 100% coverage is folklore, not a measured result. Coverage depends on what you choose to drive with tests.
TDD and Agile share an origin. Both came out of extreme programming, which is why the cycle fits a sprint so naturally.
The practical connection is story slicing. A user story too large to test is usually too large to estimate, so the red step exposes a scoping problem before the sprint absorbs it.
That makes TDD a natural fit inside agile development, where small increments are already the unit of planning.
Three mechanics matter more than the philosophy when running TDD inside agile testing.
Where teams go wrong is treating TDD as a QA activity inside an Agile process. It is a developer activity, and QA effort is better spent where unit tests cannot reach.
Note: Keep every sprint's regression suite under the time budget it needs to stay a merge gate. Explore HyperExecute
Yes. TDD produces unit tests as its output, but the relationship runs one way only: writing unit tests after the code is not TDD, because the ordering that gives the practice its value is missing.
That makes unit testing a component of TDD rather than a synonym for it.
Both end with unit tests in the repository. The difference is when the test was written, and therefore what it can tell you.
A test written first has been observed failing, so you know it can fail. A test written afterwards has usually only ever been seen passing.
That distinction matters more than it sounds. A test that cannot fail is a line of coverage that will never catch a regression.
Choose by the state of the codebase rather than by preference.
TDD has two siblings that are constantly confused with it. Both extend the test-first idea, but to different audiences and at a different altitude.
ATDD starts from a single acceptance test that expresses a requirement from the user's point of view. The developer then writes only enough production code to satisfy it.
The hub on ATDD in software testing covers the three-amigos workshop that produces those acceptance tests.
The test evaluates system behaviour rather than a unit, which is why it is often discussed alongside behaviour driven development by Selenium testing with Gherkin.
The audience is what separates it from Developer TDD. An acceptance test is written to be read by a product owner, so it names business outcomes rather than functions.
Three properties make ATDD worth the coordination cost it adds.
Developer TDD is the practice this page has described throughout: one unit test, then just enough production code to pass it.
When someone says TDD without qualification, this is what they mean.
Both variants specify requirements just in time, meaning you define only what the current increment needs rather than the whole system upfront.
What Developer TDD adds on top of ATDD comes down to three things.
BDD derives from TDD but shifts the subject from units to system behaviour. Test cases use the Given-When-Then structure so non-developers can read them.
Scenario syntax and tooling are covered in the behavior driven development hub.
Consider a scenario where the user is trying to login.
ATDD and BDD sound alike and both use plain English. The difference is emphasis: BDD describes how a feature behaves, while ATDD confirms a requirement was met.
Java teams commonly implement BDD using JBehave testing, which maps Given-When-Then scenarios directly to Java step definitions.
Where I have seen BDD earn its keep is on features with contested requirements, because the scenario becomes the place the argument happens instead of the pull request.
Where it fails is when developers write the scenarios alone. At that point you have Given-When-Then flavoured unit tests and a slower runner, with none of the shared understanding that justified the syntax.
Treating these as a choice is the wrong frame. The TDD vs BDD comparison matters less than deciding which altitude each one covers on your project.
A third acronym gets pulled in often enough to settle here: Domain-Driven Design.
TDD is a coding discipline about ordering. BDD is a collaboration practice about shared language.
DDD, introduced by Eric Evans, is a design approach: model the software around the business domain using vocabulary shared with domain experts.
| Parameters | TDD | BDD | DDD |
|---|---|---|---|
| Full name | Test-Driven Development | Behavior-Driven Development | Domain-Driven Design |
| What it drives | How you write code | How the team agrees on behaviour | How you model the problem |
| Main question | Does this unit do what I expect? | Does the system do what the business wants? | Does our model reflect the real domain? |
| Written by | Developers | Product, QA, and developers together | Developers with domain experts |
| Typical artifact | Unit tests in an xUnit framework | Given-When-Then scenarios in Gherkin | Ubiquitous language, bounded contexts, entities |
| Level it works at | Code level | Feature level | Architecture and design level |
They compose naturally. Use DDD to decide what the objects are, BDD to agree what a feature should do, and TDD to build it.
BDD is fairly described as TDD raised to the level of behaviour. It grew out of TDD to fix a recurring problem: people kept writing tests about methods instead of about value.
Every language has an established xUnit-style runner. The choice matters less than picking one the whole team runs the same way.
Around the runner sit three supporting tool categories that a TDD workflow relies on in practice.
The unit tests TDD produces run locally in seconds. The problem arrives later, when the regression suite those cycles accumulated no longer finishes fast enough to run on every commit.
That is the point where teams start skipping step 4, and the safety net quietly stops working. TestMu AI HyperExecute addresses the runtime rather than the tests themselves.
Setup details are in the getting started with HyperExecute documentation.
Subscribe to the TestMu AI YouTube channel for more testing tutorials.
AI coding agents make writing a test almost free, which inverts the economics of TDD: the scarce skill is no longer producing tests but deciding which behaviour is worth specifying.
This is the part of TDD that changed most since Kent Beck wrote the book, and it cuts both ways.
Ask an agent to write tests for existing code and it will infer intent from the implementation. If the implementation is wrong, the generated test enshrines the bug and reports green.
Writing the test first removes that failure mode, because the specification exists before any implementation is available to copy from.
The red step is also the only cheap check that a generated test can fail at all. Run it before the implementation exists and watch it go red.
The ordering below keeps the agent useful without letting it decide what correct means.
The failure mode to watch for is volume. An agent will happily produce forty tests for a function that needed four, and a suite nobody reads is a suite nobody maintains.
Treat generated tests the way you would treat a pull request from a fast, literal-minded colleague who has never seen your production incidents.
Most TDD advice is abstract. These are the practices that survive contact with a real codebase, each paired with the mistake it prevents.
Four errors account for most abandoned TDD adoptions.
Six misconceptions come up in almost every TDD discussion. Each one is worth correcting because each leads teams to abandon the practice for the wrong reason.
Myth 1: TDD is the same as unit testing.
Reality: Unit tests are the output, not the practice. Writing unit tests after the fact is not TDD, because the ordering that makes TDD work is missing.
Myth 2: All the tests are written before any production code.
Reality: One test at a time. You write a single failing test, pass it, then move on. Writing the whole suite upfront is not TDD.
Myth 3: TDD practitioners skip design and architecture.
Reality: TDD shapes design at the unit level. It says nothing about system architecture, and whiteboarding remains as necessary as ever.
Myth 4: TDD is a test strategy that can replace QA.
Reality: The name is misleading. TDD is a development technique, and it covers no smoke, load, exploratory, or end-to-end testing.
Myth 5: TDD slows teams down.
Reality: Partly true, and worth being honest about. Initial development time rises 15% to 35% in the Microsoft and IBM data, with defect density falling 40% to 90%.
You are trading build time for debug time. Whether that trade is worth making depends on the cost of a defect in your domain.
Myth 6: The goal is 100% test coverage.
Reality: Coverage is a byproduct, not a target. Chasing the number produces tests written to touch lines rather than to verify behaviour.
Test coverage measures which lines or branches your tests actually executed. In TDD it works as a diagnostic and never a goal, since a high number says nothing about whether the assertions are real.
Tooling for measuring it is covered in the test coverage hub.
Done properly, TDD produces high coverage as a side effect, because no production line exists without a test that demanded it.
Read it in one direction only. Low coverage reliably indicates untested code. High coverage does not indicate correctness.
A test that calls a function and asserts nothing still counts as covered, which is why mandated coverage targets tend to produce exactly those tests.
I once reviewed an 85% coverage report on a service with no assertions on its error paths at all.
Use branch coverage rather than line coverage where your tooling supports it, and treat sudden drops as a review signal rather than a build failure.
For how the instrumentation actually works, the code coverage hub goes through statement, branch, and path metrics.
Test-driven development is narrower than its reputation suggests, and more useful because of it. Write the failing test, pass it minimally, then improve the design while the suite holds.
The evidence supports it where it fits: defect density down 40% to 90% across the Microsoft and IBM teams, at the cost of 15% to 35% more initial development time.
That trade is worth making for pricing, permissions, validation, and algorithms. It is not worth making for spikes, thin glue code, or volatile UI.
If you want to start today, pick the next bug report you receive and reproduce it with a failing test before you fix it. That single habit is TDD in miniature, and it pays immediately.
From there, the test case hub covers how to express the behaviour each cycle should specify.
Author
Piyusha Podutwar is a Senior Software Engineer at DPS with over 12 years of experience in mainframe application and system programming. She has authored 20+ technical tutorials for TestMu AI on API testing, Agile, DevOps automation, software testing, automation testing, and digital transformation. She is skilled in Assembler, COBOL, DB2, and JCL, and has led large-scale modernization and migration projects across banking, finance, retail, and insurance domains. A Certified Scrum Master, Piyusha previously worked with IBM, TCS, BMC Software, and T-Systems.
Reviewer
Himanshu Sheth is the Director of Marketing (Technical Content) at TestMu AI, with over 8 years of hands-on experience in Selenium, Cypress, and other test automation frameworks. He has authored more than 130 technical blogs for TestMu AI, covering software testing, automation strategy, and CI/CD. At TestMu AI, he leads the technical content efforts across blogs, YouTube, and social media, while closely collaborating with contributors to enhance content quality and product feedback loops. He has done his graduation with a B.E. in Computer Engineering from Mumbai University. Before TestMu AI, Himanshu led engineering teams in embedded software domains at companies like Samsung Research, Motorola, and NXP Semiconductors. He is a core member of DZone and has been a speaker at several unconferences focused on technical writing and software quality.
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance