November 29, 2025 (1mo ago)

A Guide to Red Green Refactor TDD

Master the red green refactor TDD cycle with this practical guide. Learn the workflow, explore real-world examples, and build higher-quality, maintainable code.

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

Master the red green refactor TDD cycle with this practical guide. Learn the workflow, explore real-world examples, and build higher-quality, maintainable code.

Red-Green-Refactor TDD: Practical 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 simple, disciplined workflow that helps teams design and ship reliable software. Start with a failing test (Red), write the minimum code to pass it (Green), then clean up the implementation (Refactor). Repeating this loop turns uncertainty into small, verifiable steps that improve quality and reduce risk.

The Rhythm of Test‑Driven Development

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

Many developers assume TDD is only about testing, but 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 approach reduces guesswork and encourages small, deliberate progress. The Red‑Green‑Refactor cycle becomes the heartbeat of reliable development.

Understanding the Three Stages

Each stage has a focused purpose and keeps the work small and verifiable.

  • Red phase (failing test): Write a single automated test that represents the smallest useful behaviour. The test fails because the implementation doesn’t exist yet. The failure proves the test is valid.
  • Green phase (make it pass): Implement the smallest amount of code needed to satisfy the test. Prioritize simplicity over elegance to avoid over‑engineering.
  • Refactor phase (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 non‑negotiable. 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. In some regions, many teams have already incorporated TDD into their practices; adoption patterns and outcomes vary by market and organization1.

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.

To see the cycle in practice, we’ll build a practical UI example: a LikeButton component using TypeScript, React, and Jest. This example shows how TDD guides design while keeping behaviour predictable.

Red phase: define the first requirement

The simplest requirement is: the component renders without crashing and displays “Like.” We 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 quickly 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.

A disciplined TDD practice has been linked to measurable quality improvements and reduced debugging effort in empirical studies and industry reports2.

Reducing post‑release defects and maintenance costs

By writing tests before the code, you only add production code that is required to satisfy a test. This creates a robust safety net and reduces unexpected regressions. Several studies and industry reports document lower defect rates and less time spent on rework for teams that consistently apply TDD and automated testing practices2.

A focus on quality upfront lowers the total cost of ownership for software, because you avoid 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 ends up exercising 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 are what 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. Ask, given this input, what is the expected output? Tests that verify behaviour 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 CI. 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.

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 test 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.

Q&A — Common user questions

Q: How long before my team sees value from TDD?

A: Value appears quickly in reduced regression bugs and faster debugging. The pace depends on team size and discipline, but small, consistent wins often show up within a few sprints.

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

A: Start with a single new feature or noncritical bug. Require a failing test before implementation and enforce the refactor step.

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

A: Present the long‑term cost savings: fewer production incidents, lower maintenance costs, and faster feature delivery over time. Use concrete incidents your team has faced as 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.