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

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
| Phase | Purpose | Developer goal |
|---|---|---|
| Red | Define the requirement and validate the test | Write one small test that fails |
| Green | Satisfy the requirement | Add the minimum code to make the test pass |
| Refactor | Improve internal quality | Clean 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

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

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

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