BugMojoBugMojoBugMojo
FeaturesPricingBlogHelpAbout
Add to ChromeLog inGet started
BugMojoBugMojo

Bug reports that actually help fix bugs — capture, replay, share.

A product of Softech Infra.

Product

  • Features
  • Pricing
  • Browser extension
  • Get started
  • Log in

Resources

  • Help & guides
  • Blog
  • Compare
  • Glossary

Company

  • About
  • Contact
  • Security
  • Privacy
  • Terms
  • Sitemap
© 2026 BugMojo. All rights reserved.
AllGuidesEngineeringPlaybooksCompareGlossaryAlternativesBy roleBug tracking by framework
  1. Home
  2. Blog
  3. Guides
  4. What Is Test-Driven Development (TDD)?
Guide

What Is Test-Driven Development (TDD)?

Test-driven development flips the usual order: you write a failing test first, write just enough code to make it pass, then refactor. Here is the red-green-refactor cycle with worked examples, the three laws, the real benefits, and the honest limits.

ManviManvi·Jul 16, 2026·11 min read
Guides
Isometric line-art red-green-refactor test-driven-development cycle looping through a failing test, a passing test, and a refactor, lime on a dark canvas
TL;DR
  • TDD writes the test first. Before you write any production code, you write a small automated test that fails because the behavior does not exist yet.
  • The loop is red-green-refactor. Fail (red), write the minimum code to pass (green), then clean up (refactor) with the test as a safety net.
  • The test is a spec. Writing it first forces you to define exactly what the code should do before you decide how.
  • Coverage is a byproduct. A comprehensive regression suite falls out of the process, making later refactoring safe.
  • It has limits. TDD tests what you thought of. It does not replace exploratory testing or catch integration and UX problems — and the bugs that still reach users need reproduction.

Most people write code first and tests later — if they write tests at all. Test-driven development inverts that order, and the inversion is the whole point. In TDD you write a failing test before you write the code that makes it pass, then you write only the minimum code needed to turn that test green, and then you refactor. It sounds backwards the first time you hear it, but the discipline produces three things at once: a precise definition of what you are about to build, a safety net that catches regressions the moment they appear, and code that is decoupled enough to be tested in isolation.

TDD was popularized by Kent Beck as part of Extreme Programming, and its rhythm — red, green, refactor — is now standard vocabulary across the industry. This guide explains what TDD is, walks the cycle step by step with two worked examples, covers Uncle Bob's three laws, and is honest about where TDD helps and where it does not.

What test-driven development actually is

Test-driven development is a practice where you write a small automated test for a behavior before writing the code that implements it. The test fails first, you write the minimum code to make it pass, then you refactor. The test is written from the caller's side, so it doubles as an executable specification of what the code must do.

The key shift is that the test comes first, and that changes what a test is for. When you write a test after the code, the test asks "does this code do what it happens to do?" — it tends to lock in whatever you already built, bugs included. When you write the test first, it asks "what should this code do?" — you have to state the desired behavior, the inputs, and the exact expected output before a single line of implementation exists. The test becomes a specification you can execute, not an afterthought.

TDD operates at a small scale. You do not write one giant test for a whole feature; you write one tiny test for one slice of behavior, make it pass, and then write the next one. Progress happens in a series of short loops, each usually only a few minutes long, and each ending with all tests green.

The red-green-refactor cycle, step by step

The engine of TDD is a three-phase loop. Each phase has exactly one job, and you do not move to the next phase until the current one is done.

  • Red. Write a small test for behavior that does not exist yet. Run it and watch it fail. The failure matters: it confirms the test actually exercises something and is not accidentally passing. A test that has never been red is a test you cannot trust.
  • Green. Write the simplest code that makes the test pass. This is not the time for elegance — hardcoding a return value or writing something obviously naive is allowed and even encouraged. The only goal is to get to green as fast as possible.
  • Refactor. Now that the test is passing, clean up. Remove duplication, improve names, extract functions, simplify. Because the test is green, it acts as a safety net: if a refactor breaks behavior, the test goes red immediately and tells you.

Then you loop back to red with the next slice of behavior. Let us watch the loop run on a real function.

Red: write a failing test

Say we need an isValidEmail function. In TDD we start with the test, before the function exists. We assert the smallest useful behavior first: a plainly valid address returns true.

isValidEmail.test.jsjavascript
import { isValidEmail } from './isValidEmail';

test('accepts a normal, valid address', () => {
  expect(isValidEmail('user@example.com')).toBe(true);
});

test('rejects a string with no @ sign', () => {
  expect(isValidEmail('user.example.com')).toBe(false);
});

test('rejects a string with no domain', () => {
  expect(isValidEmail('user@')).toBe(false);
});

Run it now and it fails — isValidEmail is not even defined. That is a good, honest red: the test fails for the right reason. Now, and only now, do we write code.

Green: write the minimum code to pass

We write the simplest implementation that turns all three assertions green. No extra RFC-compliance heroics, no features nobody tested for — just enough to pass.

isValidEmail.jsjavascript
export function isValidEmail(input) {
  if (typeof input !== 'string') return false;
  const at = input.indexOf('@');
  if (at < 1) return false;              // needs something before @
  const domain = input.slice(at + 1);
  return domain.includes('.') && domain.length > 2;
}

Run the suite: all green. Notice we did not try to handle every exotic edge case up front. TDD deliberately builds only what a test asked for. If we later discover that user@localhost or a plus-addressed alias needs specific handling, that starts as a new failing test — a new red — not a speculative branch we bolt on now.

Refactor: clean up under a green bar

The tests pass, so we can safely improve the code. Here we tidy the logic into clearer named checks. The behavior must not change — and the tests are what guarantee it did not.

isValidEmail.jsjavascript
export function isValidEmail(input) {
  if (typeof input !== 'string') return false;

  const [local, domain, ...rest] = input.split('@');
  if (rest.length > 0) return false;      // more than one @

  const hasLocal = local.length > 0;
  const hasDomain = Boolean(domain) && domain.includes('.') && domain.length > 2;

  return hasLocal && hasDomain;
}

Re-run the suite: still green. We changed the shape of the code with confidence because the tests were watching. That is the loop — red, green, refactor — and you repeat it slice by slice. Add a test for a new rule (it goes red), make it pass (green), tidy up (refactor), and go again. Over a session you accumulate both working code and a growing suite that pins its behavior in place.

The three laws of TDD

Robert C. Martin ("Uncle Bob") tightened the discipline into three laws that describe the loop at a very fine grain:

  1. You may not write production code until you have written a failing test. The test always comes first.
  2. You may not write more of a test than is sufficient to fail — and not compiling counts as failing. The moment the test fails, you stop writing test and switch to code.
  3. You may not write more production code than is sufficient to pass the one failing test. As soon as it is green, you stop writing code and either refactor or write the next failing test.

Followed strictly, the three laws lock you into cycles that last seconds to a minute or two. Most developers use them as a north star rather than a rigid ritual, but they capture the essential idea: never let production code get ahead of a test that demanded it.

The benefits of TDD

The advantages of TDD compound as a codebase grows:

  • It forces you to define behavior first. Writing the test before the code makes you decide what the function should do — its inputs, outputs, and edge cases — before you get lost in how to implement it. Ambiguity surfaces early, at the cheapest possible moment.
  • High test coverage is a byproduct. Because every line of production code exists to satisfy a test, you end up with a comprehensive suite without ever running a separate "write the tests" phase. Coverage is not a chore; it is a side effect.
  • Refactoring becomes safe. A green suite is a safety net. You can restructure, rename, and optimize aggressively, because any behavior you accidentally break turns a test red instantly. This is what Martin Fowler calls self-testing code, and it is the precondition for refactoring at all.
  • It improves design. Code that must be tested in isolation has to be callable in isolation — which pushes you toward small units, clear interfaces, and loose coupling. Hard-to-test code is usually a symptom of a design problem, and TDD makes you feel that pain immediately rather than months later.
  • Feedback is fast. The red-green loop tells you within seconds whether the thing you just wrote works, so defects are caught at the moment of introduction instead of days later in QA or, worse, in production.

The honest limitations of TDD

TDD is a powerful practice, not a silver bullet, and it is worth being clear-eyed about where it does not help.

  • It is not free. Writing tests first is slower up front. You are trading immediate speed for long-term safety and design quality, and on some work that trade is not worth it.
  • It is a poor fit for exploratory or throwaway work. When you are prototyping, spiking on an unfamiliar API, or building something you plan to discard, writing tests first mostly gets in the way. TDD shines on code whose behavior you can specify; it fights you when you are still discovering what the behavior should be.
  • UI and visual work resist it. "The button should feel right" is not a unit test. Layout, animation, and look-and-feel are hard to pin down with the kind of assertions TDD relies on.
  • It only tests what you thought of. This is the big one. A TDD suite verifies the cases you imagined when you wrote the tests. It cannot catch the bug in the scenario that never occurred to you. TDD does not replace exploratory testing, and unit-level TDD says nothing about how your components behave together — that is the domain of integration and end-to-end testing. A function can be perfectly unit-tested and still break the moment it meets the real database, the real API, or a real user.

In other words, TDD is one layer of defense. It makes the units you specified rock-solid; it does not tell you whether you specified the right units, whether they integrate, or whether the experience holds up in the hands of a real person.

TDD vs BDD

People often mention behavior-driven development (BDD) in the same breath as TDD, and the two are related but not the same. TDD is a developer practice: write a unit test, write the code, refactor. Its tests are developer-facing and usually verify small units of code. BDD keeps the test-first idea but shifts the focus to externally observable behavior, described in a shared, business-readable language — commonly Given / When / Then scenarios that a product owner, tester, and developer can all read and agree on.

login.featuregherkin
Feature: Sign in

  Scenario: Successful sign in with valid credentials
    Given a verified account "user@example.com" exists
    And the user is on the login page
    When they sign in with the correct password
    Then they land on the dashboard
    And their name is shown in the header

Roughly: TDD tends to answer "is this unit of code correct?" while BDD tends to answer "does the system do what the business asked for?" They are complementary layers rather than competitors — many teams write BDD scenarios to frame a feature and TDD unit tests to build the pieces inside it. If you are formalizing what "done" means for a feature, our guide on writing acceptance criteria that prevent bugs and on how to write test cases covers the same specify-behavior-first instinct from the QA side.

Where TDD ends and reproduction begins

TDD is one of the best tools we have for preventing bugs at the unit level. If you practice it well, a whole class of defects — the off-by-one, the unhandled null, the regression you would otherwise reintroduce — simply never ships, because a red test stops it at your desk. That is real, and it is worth the discipline.

But recall the honest limitation: TDD only tests what you thought of. The bugs that survive TDD are precisely the ones nobody imagined — the integration edge case, the weird real-world input, the interaction between two features that were each individually green. Those bugs reach users, and when they do, the bottleneck is no longer prevention but reproduction: a developer cannot fix what they cannot reproduce. This is the gap BugMojo closes. When a bug slips past your tests and a user hits it, one capture records the rrweb session replay, the console logs, and the network requests, so the failing scenario you never wrote a test for arrives fully reproduced — ready to become the next red test on the way to a fix.

Key takeaway

Test-driven development means writing a failing test first, writing the minimum code to pass it, then refactoring under a green bar — the red-green-refactor loop. It forces you to define behavior first, yields high coverage and safe refactoring as byproducts, and pushes you toward better-decoupled design. But it only tests what you thought of: it does not replace exploratory or integration testing, and the bugs that still reach users need reproduction, not just prevention.

⁓ ⁓ ⁓
For the bugs your tests never imagined

TDD stops the bugs you thought of. For the ones that reach users, capture a session replay plus console and network in one click — a complete reproduction, ready for a developer or an AI agent over MCP.

Install the free extension

Frequently asked questions

Frequently asked questions

Sources

  1. Martin Fowler — Test Driven Development: the origin of the red-green-refactor rhythm and the reasoning behind writing tests first — martinfowler.com (2026)
  2. Test-driven development — overview of the TDD cycle, history, the three laws, and its benefits and limitations — Wikipedia (2026)
  3. Agile Alliance glossary — TDD as an Agile engineering practice and its place in the development workflow — Agile Alliance (2026)
  4. Martin Fowler — Self-Testing Code: why a comprehensive automated test suite (the byproduct of TDD) is what makes refactoring safe — martinfowler.com (2026)
Share:
Manvi
Manvi· QA Tester

Manvi is a Quality Assurance Tester with three years of experience. For her, quality is not just about finding bugs — it is about ensuring the best possible experience for every user.

On this page

  • What test-driven development actually is
  • The red-green-refactor cycle, step by step
  • Red: write a failing test
  • Green: write the minimum code to pass
  • Refactor: clean up under a green bar
  • The three laws of TDD
  • The benefits of TDD
  • The honest limitations of TDD
  • TDD vs BDD
  • Where TDD ends and reproduction begins

Get bug-tracking insights, weekly.

Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.