November 29, 2025 (5mo ago)

Red Green Refactor TDD 指南

通过本实用指南掌握 Red-Green-Refactor TDD 循环。学习工作流程,探索真实示例,并构建更高质量、易维护的代码。

← Back to blog
Cover Image for Red Green Refactor TDD 指南

通过本实用指南掌握 Red-Green-Refactor TDD 循环。学习工作流程,探索真实示例,并构建更高质量、易维护的代码。

Red-Green-Refactor TDD:实用指南

摘要: 通过本实用指南掌握 Red-Green-Refactor TDD 循环,了解实际工作流程、示例及其业务优势,以构建更清晰、可维护的代码。

介绍

Red-Green-Refactor 测试驱动开发(TDD)循环是一种简单且自律的工作流程,帮助团队设计并交付可靠的软件。以一个失败的测试开始(Red),编写通过该测试所需的最少代码(Green),然后清理实现并改进结构(Refactor)。重复这一循环可以将不确定性转化为一系列小而可验证的步骤,从而提高质量并降低风险。

测试驱动开发的节奏

用手绘图说明测试驱动开发 (TDD) 工作流中的 Red-Green-Refactor 循环。

许多开发者以为 TDD 只是关于测试,但它主要是一种设计实践。先写测试会迫使你在实现之前思考代码将如何被使用,从而颠倒了通常的开发流程。这种方法减少了猜测,鼓励小而有意识的进步。Red-Green-Refactor 循环成为可靠开发的节拍。

理解三个阶段

每个阶段都有明确的目的,并将工作保持为小而可验证的单位。

  • Red 阶段(测试失败):编写一个自动化测试,代表最小的有用行为。测试会失败,因为实现尚不存在。失败证明测试有效。
  • Green 阶段(使之通过):实现满足测试所需的最少代码。优先考虑简单性而不是优雅,以避免过度设计。
  • Refactor 阶段(改进代码):在测试通过作为安全网的情况下,清理命名、消除重复并改善结构,同时不改变行为。

重构步骤是不可妥协的。跳过它会积累技术债务,使未来的更改更困难。

Red-Green-Refactor 一览

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

采用这种节奏有助于团队以可预测且自信的方式前进。在某些地区,许多团队已将 TDD 纳入其实践;不同市场和组织的采用模式与结果会有所不同1

在实践中演示 TDD 循环

手写图示,说明一个带有红心图标、文本框和绿色圆圈图标的流程。

为了展示该循环的实践应用,我们将构建一个实用的 UI 示例:一个使用 TypeScript、React 和 Jest 的 LikeButton 组件。此示例展示了 TDD 如何在保持行为可预测的同时指导设计。

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;

再次运行测试,它们通过了。本轮循环任务完成。

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 的软件开发(许多 bug,沉重)与有 TDD 的开发(更少问题,更轻)。

TDD 的工程收益很快会转化为业务价值。生产环境中更少的缺陷意味着更低的支持成本、更少的客户流失以及更强的品牌声誉。当缺陷在早期被捕获时,修复成本更低,团队可以把更多时间花在构建新的、有价值的功能上。

在实证研究和行业报告中,严格的 TDD 实践与可衡量的质量提升和减少调试工作量有联系2

减少发布后缺陷和维护成本

通过在代码之前编写测试,你只会添加满足测试所需的生产代码。这创建了一个稳固的安全网,减少了意外回归。一些研究和行业报告记录了对持续应用 TDD 和自动化测试实践的团队来说,更低的缺陷率和更少的返工时间2

前期关注质量可以降低软件的总体拥有成本,因为你能避免技术债务随时间累积的复合成本。

加速入职并提高可预测性

全面的测试套件可作为可执行的文档。新开发者可以运行测试来了解系统的预期行为,而不是依赖过时的维基文档。这缩短了上手时间并减轻了资深工程师的负担。

一致的 TDD 实践也提高了可预测性。将工作拆分为许多小的、有测试保护的循环,使估算和进度跟踪更可靠,这有助于规划和与利益相关者的沟通3

常见的 TDD 陷阱及其规避方法

TDD 很容易描述,但要掌握却很微妙。以下反模式常常削弱 TDD 的好处。

把集成测试穿成单元测试的外衣

问题:测试最终涉及了许多活动部件——组件、其服务、API 客户端,甚至数据库。测试变得缓慢、易碎且噪声大。

修正:以隔离方式测试单个单元。对外部依赖使用 mock、stub 和 fake。如果需要集成覆盖,编写专门的集成测试并分开运行。

真正的单元测试不应触及网络、文件系统或真实数据库。它的速度和可靠性让你能无畏地重构4

测试实现细节而不是行为

问题:测试断言内部细节而不是公共行为。当你改进或重构内部时,测试会失败,即便行为仍然正确。

修正:测试公共 API 和可观察的效果。问自己,给定这个输入,期望的输出是什么?验证行为的测试可以抵抗无关的重构,并作为有价值的文档。

跳过重构步骤

问题:开发者在让测试通过后匆忙进入下一个功能,留下混乱的实现。

修正:把重构视为必需步骤。在测试通过的前提下,进行小的清理是安全的,并会逐步累积成一个易于更改的代码库。

将 TDD 集成到你的团队与遗留代码中

手绘图示,展示开发者在遗留代码上使用表征测试并结合 CI/CD 工作。

采纳 TDD 是一种文化变革,同时也是技术上的改变。鼓励实践操作学习,把测试作为团队完成定义的一部分,并庆祝测试驱动的胜利。

在团队内推广 TDD

一些实用方法以建立势头:

  • 结对编程,让有经验的开发者引导队友完成 Red-Green-Refactor 循环。
  • 对于更难的问题采用群体编程(mob programming),轮流驱动以传播知识。
  • 午餐学习会,演示代码库中真实的 TDD 示例。

从小处开始,让早期的测试捕获成为团队的证明点。

用表征测试驯服遗留代码

当代码没有测试且改动风险较大时,编写表征测试以记录当前行为。这些测试让你可以有信心地重构或添加功能,先断言系统今天的行为如何。

用 CI/CD 自动化质量保证

在每次提交时于 CI 中运行测试套件。这提供即时反馈,执行质量门禁,并使通过测试成为合并前的必要步骤。自动化使测试反馈循环快速且可靠。

关于 TDD 的常见问题解答

TDD 会替代其他类型的测试吗?

不会。TDD 侧重于将单元测试作为一种设计工具,但你仍然需要集成测试和端到端测试来验证组件间的交互和完整的用户流程。

我如何在有数据库或外部 API 的情况下使用 TDD?

通过使用 mock、stub 或 fake 将代码与外部依赖隔离。在一个独立的“气泡”中测试你的逻辑,并将真实的集成测试保留在单独的测试套件中。

测试简单的 UI 组件值得吗?

值得,只要你测试的是行为而不是实现。验证用户所看到和所做的事情,例如按钮是否渲染了正确的标签,或在点击时是否触发了正确的操作。

问答 — 常见用户问题

问:我的团队需要多长时间能看到 TDD 带来的价值?

答:价值会很快在减少的回归 bug 和更快的调试中显现。速度取决于团队规模和纪律性,但小而持续的胜利通常会在几次冲刺内出现。

问:采纳 TDD 的最小第一步是什么?

答:从单个新功能或非关键 bug 入手。在实现之前要求一个失败的测试,并强制执行重构步骤。

问:我如何说服利益相关者投入时间编写测试?

答:展示长期的成本节约:更少的生产事故、更低的维护成本以及随时间推移更快的功能交付。使用你团队曾遇到的具体事故作为示例。

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编写代码。
您让它持久。

在AI加速的时代,干净代码不仅仅是好的实践 — 它是能够扩展的系统与在自己的重量下崩溃的代码库之间的区别。