Red‑Green‑Refactor TDD 是把大问题拆成小步的设计节奏:先写会失败的测试(Red),实现最少代码通过测试(Green),然后在测试保障下重构(Refactor)。本指南通过实践示例与常见陷阱,帮助你把 TDD 融入日常开发,提升代码质量并降低长期成本。
November 29, 2025 (8mo ago) — last updated June 16, 2026 (2mo ago)
Red‑Green‑Refactor:TDD 实用指南
掌握 Red‑Green‑Refactor TDD 循环、工作流程与实战示例,帮助你编写更可靠、可维护的代码并降低维护成本。
← Back to blog
Red-Green-Refactor TDD:实用指南
摘要: 通过本实用指南掌握 Red‑Green‑Refactor TDD 循环,了解实际工作流程、示例及其业务优势,以构建更清晰、可维护的代码。
介绍
Red‑Green‑Refactor 测试驱动开发(TDD)是一个轻量且自律的开发节奏,能把大问题拆解为一系列小而可验证的步骤。先写一个会失败的测试(Red),实现使其通过的最小代码(Green),然后在测试保障下清理和改进实现(Refactor)。长期坚持可提升代码质量、减少回归,并加快交付节奏。
为什么采用 TDD?
TDD 不只是测试,它是一种设计方法。先写测试迫使你在实现前思考 API 与行为,从而减少猜测并鼓励小步迭代。许多团队在将 TDD 与自动化测试、CI/CD 流程结合后获得了更稳定的交付能力与更少的生产缺陷1。研究也表明,TDD 可带来可测量的质量提升并降低调试工作量2。把测试作为设计工具,有助于团队构建更可维护的代码库并缩短新人入职时间。
Red‑Green‑Refactor 三阶段详解
每个阶段专注一个目标,保持工作小且可验证:
- Red(测试失败):写一个代表最小有用行为的自动化测试,验证测试本身有效。
- Green(使之通过):编写满足测试的最少代码,优先简单实现而非过度设计。
- Refactor(重构):在测试保护下改进命名、消除重复、优化结构,不改变外部行为。
跳过重构会快速累积技术债务;将重构视为必需步骤能长期保持代码健康。
阶段一览
| 阶段 | 目的 | 开发者目标 |
|---|---|---|
| Red | 定义需求并验证测试 | 编写一个小且会失败的测试 |
| Green | 满足需求 | 添加最少代码以通过测试 |
| Refactor | 提高内部质量 | 清理重复并澄清意图 |
采用这一定律化的节奏能让团队更可预测地交付,并把大改动拆成可控的小步。
在实践中演示 TDD 循环
下面通过一个简单的 TypeScript + React + Jest 示例,演示如何在 UI 组件上实践 Red‑Green‑Refactor。示例展示从失败的测试开始,逐步实现并重构。
Red:定义第一个需求
第一个最小需求是组件能够渲染且显示 “Like”。在实现之前先写测试:
// 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();
});
});
运行测试会失败(组件尚不存在),这就是 Red 阶段的目的。
Green:仅实现通过测试所需的最少内容
实现简单组件以通过测试:
// LikeButton.tsx
import React from 'react';
const LikeButton = () => {
return <button>Like</button>;
};
export default LikeButton;
再次运行测试,测试通过,完成一次 Red‑Green 循环。
Refactor:打磨实现
在测试通过的基础上改进代码结构与类型定义:
// LikeButton.tsx (refactored)
import React, { FC } from 'react';
type LikeButtonProps = {};
const LikeButton: FC<LikeButtonProps> = () => {
return <button>Like</button>;
};
export default LikeButton;
测试仍然通过,说明重构在安全网下进行。
迭代示例:点击按钮
新增需求:点击后文本变为 “Liked” 并禁用按钮。先写失败测试:
// 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();
});
实现最小行为:
// 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;
通过测试后继续重构并逐步添加边界条件与无障碍支持。
代码质量与业务价值
采用 TDD 与自动化测试能把工程收益转化为业务价值。更少的生产缺陷意味着更低的支持成本、更少的客户流失,以及更强的品牌信任。将测试组合到持续集成与交付流程中还能显著提升交付频率与可靠性——顶尖团队在 CI/CD 与自动化实践上有明显优势,例如更高的部署频率和更短的变更周期3。
研究表明,持续采用 TDD 与自动化测试的团队在缺陷率和调试时间上有可测量的改善,而这些改进最终带来更低的总体拥有成本2。
常见的 TDD 陷阱与修正
以下是常见反模式及应对策略:
1. 把集成测试当单元测试
问题:测试触及网络、数据库或许多模块,导致慢、易碎的测试套件。
修正:保持单元测试隔离,使用 mock、stub 或 fake。把集成测试单独运行以覆盖跨组件交互。
2. 测试实现细节而不是行为
问题:断言内部实现导致重构时频繁破坏测试。
修正:测试公共 API 和可观察行为。关注输入与输出,而非内部变量或具体实现。
3. 跳过重构
问题:通过测试后直接进入下一个功能,留下低质量实现。
修正:把重构作为循环的一部分;在测试保护下逐步清理,防止技术债务积累。
在团队与遗留代码中推广 TDD
采纳 TDD 不只改变写代码的方式,也需要文化与流程的支持:结对编程、群体编程、示例驱动的午餐学习会,都能帮助传播实践。对于遗留代码,先编写表征测试记录现有行为,然后在安全网下重构与替换。
把测试作为 CI 检查的一部分,在每次提交时运行测试套件,可以快速获得质量反馈并阻止有缺陷的变更进入主分支3。
常见问题解答
TDD 会替代其他类型的测试吗?
不会。TDD 强调把单元测试作为设计工具,但仍需要集成测试与端到端测试来验证跨组件交互和完整用户流程。
在有数据库或外部 API 的情况下如何使用 TDD?
对外部依赖使用 mock、stub 或 fake,在隔离的“气泡”里测试业务逻辑。把真实的集成测试放在单独的测试套件中以验证端到端行为。
测试简单的 UI 组件值得吗?
值得,只要你测试的是行为而不是实现细节。验证用户可见的效果,例如文本、状态变化和无障碍属性。
推广建议:在团队中落地的实践
- 从小处入手:在新功能或非关键缺陷上强制先写失败测试并执行重构。
- 结对与群体编程:让经验丰富的工程师示范 Red‑Green‑Refactor 循环。
- 把测试运行集成到 CI 管道:使测试成为合并前的质量门禁。
快速问答(简短版)
Q1: 我如何开始用 TDD?
A1: 从一个小功能开始:先写一个会失败的测试,添加最少代码使其通过,然后重构。把这三个步骤作为团队规范。
Q2: TDD 对业务的直接好处是什么?
A2: 更少的生产缺陷、更低的维护成本和更可预测的交付节奏,这些都能转化为长期节省和更快的功能交付。
Q3: 我如何在遗留系统中采用 TDD?
A3: 使用表征测试记录现有行为,逐步为关键路径添加测试,然后在测试保护下重构或替换模块。
AI编写代码。您让它持久。
在AI加速的时代,干净代码不仅仅是好的实践 — 它是能够扩展的系统与在自己的重量下崩溃的代码库之间的区别。