November 29, 2025 (8mo ago) — last updated July 13, 2026 (1mo ago)

Red‑Green‑Refactor TDD Guide

Master the Red‑Green‑Refactor TDD cycle: workflow, examples, and business benefits to build maintainable, high‑quality code.

← Back to blog
Cover Image for Red‑Green‑Refactor TDD Guide

The Red‑Green‑Refactor TDD cycle turns uncertainty into small, verifiable steps: write a failing test, make it pass with minimal code, then refactor safely. Use this guide to learn the workflow, see a practical React + TypeScript example, and understand the business benefits.

Red‑Green‑Refactor TDD Guide

Summary: Master the Red‑Green‑Refactor TDD cycle with practical workflow, examples, and business benefits to build cleaner, maintainable code.

Introduction

The Red‑Green‑Refactor Test‑Driven Development (TDD) cycle is a focused workflow that turns uncertainty into small, verifiable steps. Start with a failing test (Red), write the minimum code to pass it (Green), then clean up the implementation (Refactor). This disciplined loop improves design, reduces regressions, and helps teams deliver predictable, higher‑quality software.

The Rhythm of Test‑Driven Development

A hand-drawn diagram illustrating the Red-Green-Refactor cycle for Test-Driven Development (TDD) workflow.

TDD isn’t just about testing; it’s primarily a design practice. Writing the test first forces you to think about how the code will be used before implementation, flipping the usual development flow. This reduces guesswork and encourages small, deliberate progress. Adoption patterns vary by organization and region, but many teams report integrating TDD into their development cadence as part of broader agile practices1.

Understanding the three stages

Each stage has a single, clear purpose and keeps work small and verifiable.

  • Red (failing test): Write one automated test that represents the smallest useful behaviour. The failure proves the test is valid.
  • Green (make it pass): Implement the minimum code to satisfy the test. Prioritize simplicity to avoid over‑engineering.
  • Refactor (improve the code): With tests passing as a safety net, clean up names, remove duplication, and improve structure without changing behaviour.

The refactor step is nonnegotiable. Skipping it accumulates technical debt and makes future changes harder.

Red‑Green‑Refactor at a glance

PhasePurposeDeveloper goal
RedDefine the requirement and validate the testWrite one small test that fails
GreenSatisfy the requirementAdd the minimum code to make the test pass
RefactorImprove internal qualityClean up duplication and clarify intent

Adopting this cadence helps teams move predictably and confidently. Repeating small cycles reduces risk and improves traceability of changes2.

Walking through the TDD cycle in action

A handwritten diagram illustrating a process flow with a red heart icon, a text box, and a green circle icon.

We’ll build a small UI example: a LikeButton component using TypeScript, React, and Jest. This shows how TDD guides design while keeping behaviour predictable.

Red phase: define the first requirement

The simplest requirement is that the component renders without crashing and displays “Like.” Write the test before the component.

// LikeButton.test.tsx
import React from "react";
import { render, screen } from "@testing-library/react";
import LikeButton from "./LikeButton";

describe("LikeButton", () => {
  it("renders a button with the initial text “Like”", () => {
    render(<LikeButton />);
    const likeButton = screen.getByRole("button", { name: /like/i });
    expect(likeButton).toBeInTheDocument();
  });
});

Running the test fails because the component doesn’t exist yet. This is the Red phase—exactly what we want.

Green phase: just enough to pass

Create the minimal component to satisfy the test.

// LikeButton.tsx
import React from "react";

const LikeButton = () => {
  return <button>Like</button>;
};

export default LikeButton;

Run the tests again, and they pass. Mission accomplished for this cycle.

Refactor phase: polish the implementation

Now clean up the code. Add types and establish a pattern for future expansion.

// LikeButton.tsx (refactored)
import React, { FC } from "react";

type LikeButtonProps = {};

const LikeButton: FC<LikeButtonProps> = () => {
  return <button>Like</button>;
};

export default LikeButton;

Tests still pass. The safety net lets you improve the code with confidence.

Iteration example: clicking the button

New requirement: clicking the button changes its text to “Liked” and disables it to prevent multiple clicks. Start with a failing test.

// LikeButton.test.tsx
it("changes text to “Liked” and becomes disabled when clicked", () => {
  render(<LikeButton />);
  const likeButton = screen.getByRole("button", { name: /like/i });
  fireEvent.click(likeButton);
  expect(likeButton).toHaveTextContent("Liked");
  expect(likeButton).toBeDisabled();
});

Implement the minimal behaviour to pass the test.

// LikeButton.tsx
import React, { FC, useState } from "react";

type LikeButtonProps = {};

const LikeButton: FC<LikeButtonProps> = () => {
  const [liked, setLiked] = useState(false);
  const handleClick = () => setLiked(true);
  return (
    <button onClick={handleClick} disabled={liked}>
      {liked ? "Liked" : "Like"}
    </button>
  );
};

export default LikeButton;

Run the suite, all green. Repeat: one small requirement at a time, protected by tests.

The business case for code quality

A hand-drawn balance scale comparing software development without TDD (many bugs, heavy) to with TDD (fewer issues, lighter).

TDD’s engineering benefits translate into business value. Fewer defects in production means lower support costs, less customer churn, and a stronger brand reputation. When defects are caught early, they’re cheaper to fix, and teams can spend more time building new, valuable features. Empirical studies and industry reports have linked disciplined TDD practices to measurable quality improvements and reduced debugging effort2.

Reducing post‑release defects and maintenance costs

Writing tests before the code ensures production code exists only to satisfy a test. This creates a robust safety net and reduces unexpected regressions. A focus on quality upfront lowers total cost of ownership because it prevents the compounding cost of technical debt over time.

Accelerating onboarding and improving predictability

A comprehensive test suite serves as executable documentation. New developers can run tests to learn the system’s expected behaviour instead of relying on outdated wikis. This shortens ramp time and reduces the burden on senior engineers.

Consistent TDD practices also improve predictability. Breaking work into many small, tested cycles makes estimates and progress tracking more reliable, which helps with planning and stakeholder communication3.

Common TDD pitfalls and how to avoid them

TDD is simple to describe but subtle to master. The following anti‑patterns commonly undermine TDD’s benefits.

Integration tests in unit test clothing

Problem: A test exercises many moving parts—the component, its services, API clients, and possibly a database. The test becomes slow, brittle, and noisy.

Fix: Test a single unit in isolation. Use mocks, stubs, and fakes for external dependencies. If you need integration coverage, write dedicated integration tests that run separately. A true unit test shouldn’t touch the network, filesystem, or a real database; its speed and reliability let you refactor fearlessly4.

Testing implementation instead of behaviour

Problem: Tests assert internal details instead of public behaviour. Tests break when you improve or refactor internals, even though behaviour remains correct.

Fix: Test the public API and observable effects. Given an input, what is the expected output? Behavioural tests resist unrelated refactors and remain valuable documentation.

Skipping the refactor step

Problem: Developers rush to the next feature after getting tests to pass, leaving messy implementations behind.

Fix: Treat refactor as a required step. With tests passing, small cleanups are safe and compound into a codebase that’s easy to change.

Integrating TDD into your team and legacy code

A hand-drawn diagram showing developers working on legacy code with characterization tests and CI/CD.

Adopting TDD is a cultural change as much as a technical one. Encourage hands‑on learning, make tests part of the team’s Definition of Done, and celebrate test‑driven wins.

Championing TDD within the team

Practical ways to build momentum:

  • Pair programming, where an experienced developer guides a teammate through the Red‑Green‑Refactor cycle.
  • Mob programming for tougher problems, rotating who drives to spread knowledge.
  • Lunch‑and‑learn sessions that demonstrate real examples of TDD in your codebase.

Start small, and let early test catches become proof points for the team.

Taming legacy code with characterization tests

When code is untested and risky to change, write characterization tests to document current behaviour. These tests let you refactor or add features with confidence by first asserting how the system behaves today.

Automating quality with CI/CD pipelines

Run the test suite on every commit in continuous integration. This provides immediate feedback, enforces quality gates, and makes passing tests a mandatory step before merging. Automation keeps the test feedback loop fast and dependable and reduces integration surprises in shared branches3.

Your top TDD questions, answered

Does TDD replace other kinds of testing?

No. TDD focuses on unit tests as a design tool, but you still need integration tests and end‑to‑end tests to validate component interactions and full user flows.

How do I use TDD with databases or external APIs?

Isolate code from external dependencies by using mocks, stubs, or fakes. Test your logic in a bubble and reserve real integration tests for a separate suite.

Is it worth testing simple UI components?

Yes, when you test behaviour, not implementation. Verify what users see and do, such as whether a button renders the right label or triggers the correct action when clicked.

Quick Q&A

Q: How quickly will my team see value from TDD?

A: Many teams notice fewer regression bugs and faster debugging within a few sprints when they consistently apply TDD practices.

Q: What’s the smallest first step to adopt TDD?

A: Start with one new feature or a noncritical bug: require a failing test before implementation and make refactor a required step.

Q: How do I convince stakeholders to invest time in tests?

A: Show long‑term cost savings: fewer production incidents, lower maintenance costs, and faster feature delivery. Use recent incidents from your codebase as concrete examples.

1.
Digital.ai, “State of Agile Report,” Digital.ai, https://digital.ai/resource-center/state-of-agile-report
2.
Basili, Victor R., and others, “An Empirical Study on the Effects of Test-Driven Development,” https://link.springer.com/article/10.1007/s10664-015-9378-2
3.
Google Cloud, “State of DevOps Report,” https://cloud.google.com/devops/state-of-devops
4.
Martin Fowler, “TestDrivenDevelopment,” https://martinfowler.com/bliki/TestDrivenDevelopment.html
← Back to blog
🙋🏻‍♂️

AI writes code.
You make it last.

In the age of AI acceleration, clean code isn’t just good practice — it’s the difference between systems that scale and codebases that collapse under their own weight.

Red‑Green‑Refactor TDD Guide | Clean Code Guy