想象你请了一支施工队来盖房子。传统做法是:房子盖完、家具摆好,再请质检员来做最终验收。这时候发现承重墙偏了半米,只能推倒重来,工期翻倍、预算超支。而 TDD(测试驱动开发)的做法是:动工前,先由房东、工程师、工头三方坐下来,把整栋房子的“验收清单”逐条写清楚——每一面墙的垂直度、每一扇窗的开合次数、每一间房的电路负荷。然后才开工。每砌一段墙,立刻对照清单核验,合格才继续;不合格当场返工,绝不拖延。这个“边盖边验”的节奏,就是测试驱动开发的核心。而把这份“验收清单”从工程师黑话翻译成房东听得懂的话——“当我转动门把手,门应当轻快打开”——就是 BDD(行为驱动开发)。本文将用这套“盖房验收”的比喻,带你从零理解 TDD/BDD 的原理、分层策略、可测试性设计,以及它们在真实工程中的落地方式。

岗位关联

掌握 TDD/BDD,你会成为团队里那个“带着验收清单开工”的人——既能把质量风险消灭在早期,又能用业务听得懂的语言对齐需求,这是走向高级工程师和架构师的关键能力。

目录
  1. 可视化演示
  2. TDD基本原理:红绿重构循环
  3. BDD行为驱动:说人话的验收清单
  4. 测试金字塔策略:分层验收
  5. 可测试性设计:留好标准插座
  6. 工程工具实践:进度表与巡检制度
  7. 团队协作落地:三剑客的圆桌会议
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示TDD的Red-Green-Refactor循环流程、BDD的Given-When-Then自然语言场景框架,以及TDD测试作为CI流水线质量门禁的持续集成实践。

本节配有交互动效(进入视口后自动加载,约 3KB)。

TDD Red-Green-Refactor循环 Red: 先写测试确认失败定义期望行为 Green: 写最少代码使测试通过 RGR Refactor: 重构改善代码质量保持测试通过

TDD基本原理:红绿重构循环

想验证一面墙砌得直不直,最蠢的办法是把整栋楼盖完再拿靠尺比划——那时墙面已经刷了漆,返工等于拆楼。聪明的工头随身带一把水平尺,砌一块砖,量一次。TDD 里的“水平尺”就是测试代码,而且这把尺要先于墙存在:墙还没砌,尺已经放在那儿了。这就是“测试先行”的价值:不靠事后补救,而是用验收标准直接引导施工。

TDD 的完整循环只有三步。第一步“红灯”:为下一小块功能写一个测试,运行它,看着它失败。别怕失败——失败的红色恰恰证明“尺子真的在量新的东西”。第二步“绿灯”:写最直接、最朴实、甚至有点丑的代码,让测试通过。第三步“重构”:在测试继续通过的前提下,把刚才的丑代码擦干净,改善命名、消除重复。如此循环往复,每一轮都是一次“写尺、校准、施工”的微循环。

为什么必须“先红后绿”?如果测试一开始就通过,只有两种可能:功能早就存在,或者测试根本没测到点子上——像一个没电的水平尺,怎么摆都显示 0。先看到红色,等于确认“这件功能当前确实缺失”,然后小步实现直到变绿。每轮循环 5 到 10 分钟,几十轮下来,一堆微小但扎实的进步叠成一栋楼。

这就像工头给自己立规矩:墙上没画验收线之前,绝不先抹灰。有了这道线,后面怎么调整管线、换瓷砖,都不怕把承重搞坏——因为每动一下,尺子都会告诉你有没有歪。反馈循环的魔力就在于此:错误在刚刚萌芽时就被揪出来,而不是等到交房那天。

循环k: 红k→ 实现 绿k→ 重构 蓝k\displaystyle \text{循环}_k:\ \text{红}_k \xrightarrow{\ \text{实现}\ } \text{绿}_k \xrightarrow{\ \text{重构}\ } \text{蓝}_k
这条公式描述第 k 轮 TDD 循环:先写失败的测试(红),实现后全部通过(绿),再重构并保持通过(蓝)。它既是流程,也是节奏。
  1. 红\_k——第 k 轮先写一个会失败的测试为下一个微小功能补一条验收标准,运行测试,确认它以红色失败,代表该功能当前确实缺失。
    红k: 新增测试⇒测试失败\text{红}_k:\ \text{新增测试} \Rightarrow \text{测试失败}
  2. 实现箭头——用最朴素的代码让测试通过不追求优雅,只求最小改动,目的是尽快验证测试本身有效。
    → 实现 \xrightarrow{\ \text{实现}\ }
  3. 绿\_k——所有测试转绿新增测试与旧测试全部通过,功能达到验收线,代码进入安全状态。
    绿k: 全部测试通过\text{绿}_k:\ \text{全部测试通过}
  4. 重构箭头与蓝\_k——在绿色保护下整理代码改善结构、消除重复,每动一步都重跑测试,确认依然全绿。
    → 重构 蓝k: 测试仍绿且结构更优\xrightarrow{\ \text{重构}\ } \text{蓝}_k:\ \text{测试仍绿且结构更优}

BDD行为驱动:说人话的验收清单

房东拿到 TDD 的验收清单,发现上面写满了“当输入等于 420 时,断言输出等于 220”这种天书。他愤怒地拍桌:“我要的是收银台显示 220!”BDD 要解决的,正是这个“语言鸿沟”。软件开发中,业务方和工程师各说各话是团队最大的隐形杀手,而 BDD 给出的解药,是给验收清单定一个所有人都能懂的标准句式。

这个句式就是 Given-When-Then:Given(在什么前提下)、When(做什么动作)、Then(得到什么结果)。一句话就是一条行为规则。例如:“Given 购物车里有两件单价 120 元的书,When 我点结算,Then 应付款显示 220 元。”房东、程序员、测试员看到的是同一句话,对“做完了”的定义从源头就对齐了,不再有“我以为是这样的”的扯皮。

把这种句式写进用户故事,就构成了“带验收标准的用户故事”。每个故事不只有“我要什么”,还附带“怎么算做对了”。落到工具层,Cucumber、Behave 等框架能把自然语言场景和测试代码绑定起来:场景文件既是需求文档,又是可执行的测试套件,文档与测试从此不再分家。这让“需求变更”的成本大幅下降——改一行自然语言,跑一遍自动化,就知道有没有破坏老功能。

如果说 TDD 是给施工队一把把好用的水平尺,BDD 就是让房东、设计师、施工队三方用同一种语言,把“什么样才算交付”钉在纸上。那份纸上的每一个场景,既是一份需求文档,也是一条自动化测试,更是三方共识的结晶。

验收场景=Given[前提状态]+When[触发行为]+Then[期望结果]\displaystyle 验收场景 = \text{Given}[\text{前提状态}] + \text{When}[\text{触发行为}] + \text{Then}[\text{期望结果}]
这条公式是 BDD 场景的骨架:先设定系统状态,再触发一个动作,最后断言一个可观察的结果。所有 BDD 场景都是这个三段式的实例。
  1. Given——描述系统所处的初始状态比如“购物车里有商品”“用户已登录”“库存充足”,它决定场景从什么起点开始走。
    Given[前提状态]\text{Given}[\text{前提状态}]
  2. When——描述用户或系统触发的动作比如“点击结算”“调用支付接口”“提交表单”,它是场景的推动力。
    When[触发行为]\text{When}[\text{触发行为}]
  3. Then——描述可观察、可断言的期望结果比如“显示 220 元”“返回 200 状态码”“发送邮件通知”,它是验收是否通过的判据。
    Then[期望结果]\text{Then}[\text{期望结果}]

测试金字塔策略:分层验收

如果工头对每块砖都做整楼抗震实验,工程十年完不成;如果只等楼盖完才检查,劣质砖早就混进了地基。聪明的策略是分级验收:砖块进库抽查、墙体砌完检测、楼栋落成总验。软件测试同样需要分层——这就是“测试金字塔”的直觉来源。

测试金字塔把测试分成三层。塔底叫单元测试,只测一个函数或一个类,毫秒级完成,定位精确得像显微镜——这块砖是哪个窑烧的,一查就知道。塔中是集成测试,验证模块之间的契约,比如“墙体 A”和“梁 B”的接口是否咬合。塔尖是端到端测试,模拟真实用户走完整流程,最慢、最贵、也最宏观,像一张卫星图。

三层数量的经典比例约 7:2:1。为什么单元测试最多?因为它便宜、快、定位准,某个测试失败立刻知道是哪个函数出问题;端到端测试最贵,失败只告诉你“用户下单挂了”,还得层层排查。反过来,如果全用端到端测试,跑一次半小时,反馈太慢,团队就会逃避跑测试,质量堤坝随之溃决。所以金字塔的黄金比例不是教条,而是“代价与粒度”的平衡。

金字塔是一场“代价与粒度”的平衡:微观仪器多备,宏观仪器精用。砖块逐块验、墙面逐面查、整楼最后总验——三层配合,既不让劣质砖混进地基,也不让验收拖垮工期。

N单元:N集成:N端到端≈7:2:1\displaystyle N_{\text{单元}} : N_{\text{集成}} : N_{\text{端到端}} \approx 7 : 2 : 1
这是测试金字塔的经典数量配比:约七成单元测试、两成集成测试、一成端到端测试。比例背后的原则是“越快越便宜的测试越多写”。
  1. 单元测试占约七成覆盖每个函数的正常与异常路径,毫秒级运行,是全塔的底座。
    N单元≈70%N_{\text{单元}} \approx 70\%
  2. 集成测试占约两成验证模块间的协作契约,秒级运行,数量适中。
    N集成≈20%N_{\text{集成}} \approx 20\%
  3. 端到端测试占约一成只覆盖最关键的几条用户旅程,分钟级运行,数量最少。
    N端到端≈10%N_{\text{端到端}} \approx 10\%

可测试性设计:留好标准插座

工头验收电力系统时,不会信任大楼自己的电表——万一是坏的,读数谁信?他在墙上预留标准接口,插上自己校准过的仪表。软件里的“标准接口”就是依赖注入。一段代码难测,往往不是逻辑复杂,而是它把外部依赖焊死在体内:方法里直接 new 一个数据库连接、直接读环境变量、直接发起网络请求。要测它,等于要把整台机器拆开才能通到核心齿轮。

依赖注入的做法很简单:在构造器或方法的参数里把依赖“递进来”,而不是在内部“抓进来”。比如购物车对象不自己 new 一个折扣计算器,而是通过构造器接收一个 DiscountProvider 接口。这样测试时就能塞入一个“假折扣计算器”(测试替身),验证购物车本身的逻辑,而不必真的连数据库、开网络。

有了注入,测试时就能使用各种测试替身:Stub(桩)返回固定值,Mock(模拟)断言调用关系,Fake(假实现)用内存版轻量实现顶替数据库。再配合接口隔离(只依赖抽象而非具体实现)和单一职责(一个类只做一件事),被测对象就变成一颗可以单独拧下来校验的齿轮。团队常用一条经验判断可测试性:如果这个类测试时要 new 一堆难搞的东西,多半是设计不够好。

可测试性设计不是给测试开后门,而是让每个零件都带标准接口。房子验收不再需要拆墙,插上仪表就能读数——这是工程艺术的体现,也是 TDD 能在团队里长期运转的地基。

SUT=f(D1, D2, ⋯ , Dk),Di 由注入提供\displaystyle \text{SUT} = f(D_1,\ D_2,\ \cdots,\ D_k),\quad D_i\ \text{由注入提供}
SUT 是“被测系统”,公式表示被测对象的核心逻辑 f 依赖若干外部依赖 D₁…Dk,而这些依赖均由外部注入,测试时可替换为替身。
  1. SUT——被测系统即待验证的核心逻辑单元,通常是一个类或函数。
    SUT: 被测系统\text{SUT}:\ \text{被测系统}
  2. f——核心业务逻辑与外界隔离、状态稳定,是测试关注的重点。
    f: 核心逻辑f:\ \text{核心逻辑}
  3. D\_i——第 i 个外部依赖比如数据库、网络客户端、时钟、文件系统等不稳定因素。
    Di: 外部依赖D_i:\ \text{外部依赖}
  4. 注入——依赖通过构造器或参数传入不在内部 new 死,使测试能用替身替换真实依赖。
    Di←构造器/参数注入D_i \leftarrow \text{构造器/参数注入}

工程工具实践:进度表与巡检制度

工头每天收工会干两件事:一是统计验收清单完成了多少条,二是宣布纪律——任何人动了墙的位置,当天必须把所有验收项重跑一遍。前者对应覆盖率,后者对应持续集成。没有这两样工具,验收清单就只是墙上的一张纸,风一吹就跑了。

覆盖率(Code Coverage)回答“测试碰到了哪些代码”,公式很简单:被测试执行的代码行数除以总行数乘以 100%。但它有个先天缺陷:只统计“执行过”,不统计“断言过”。一行代码被测试摸了一下但没验证任何输出,覆盖率照样 100%。所以覆盖率是必要不充分的质量信号——太低肯定不行,高了也不代表稳,关键还得看断言是否有效。

工程落地时,pytest、JUnit、Jest 负责跑测试,Coverage.py、Istanbul 负责算覆盖率,GitHub Actions、GitLab CI 负责在每次提交代码时自动执行这一切,并设置门槛:新代码覆盖率低于 80% 直接禁止合并。机器当监工,不靠人性自觉。团队里的提问也从“你测了吗”变成“CI 绿了吗”——质量纪律被自动化固化了。

覆盖率是进度表,CI 是巡检制度。有了这两样,验收清单才不会躺在纸上,而是变成一张时刻张开的雷达网——每次提交代码,整栋楼的验收项都会自动重跑一遍,任何一道墙歪了,红灯立刻亮起来。

C=LcoveredLtotal×100%\displaystyle C = \frac{L_{\text{covered}}}{L_{\text{total}}} \times 100\%
覆盖率 C 等于被测试执行到的代码行数 L_covered 除以可执行代码总行数 L_total,再乘以 100%。它衡量“测试摸到了多少代码”,而非“验证了多少行为”。
  1. L\_covered——被测试执行到的行数测试运行过程中实际执行过的可执行语句数。
    Lcovered: 测试执行命中的行数L_{\text{covered}}:\ \text{测试执行命中的行数}
  2. L\_total——可执行代码总行数不含空行和注释,只统计可被运行的语言语句。
    Ltotal: 全部可执行代码行L_{\text{total}}:\ \text{全部可执行代码行}
  3. 比值乘以 100%把比例转为百分比,便于团队设定门槛(如 80%)。
    ×100%⇒覆盖率百分比\times 100\% \Rightarrow \text{覆盖率百分比}

团队协作落地:三剑客的圆桌会议

一栋楼要让房东满意,光靠工头记笔记不够——房东心里想的“卧室朝南”,工程师理解成“主梁偏南 3 度”,两人吵到交房那天才崩溃。BDD 把三方拉到同一张圆桌上:业务方、开发、测试,逐条把验收场景念出来,任何一方说“不对”就当场改,直到三方点头。这个“三剑客”协作模式,是 BDD 的灵魂。

这个过程叫实例化需求(Specification by Example):不要抽象的描述,要具体的例子。讲“高额订单有折扣”不如讲“订单 1200 元减 80 元”来得赤裸。每个例子就是一个待实现的测试场景,写完之后,这个场景既是需求也是测试,三剑客在桌边达成的共识被固化成可执行文件,谁也不能事后抵赖。

长期运作还需要测试维护文化和演进式设计。测试代码不是一次性的,它和产品代码一样需要重构,否则会在需求变更时变成沉重的负担。演进式设计则放弃一次到位的“大爆炸”规划,靠 TDD 小步快跑,让架构随真实需求生长。就像工头不迷信一次画齐所有图纸,而是边砌边修订,用每天的红绿循环滚动微调——测试文化保证这些调整有安全网,演进式设计保证系统不会僵死。

一张圆桌,三方点头,一套会自我演进的安全网。TDD/BDD 最终不是某种工具,而是一个团队共同写就、共同守护的“房子使用说明书”——写在纸上的每一个字,都是大家同意过的、可以兑现的承诺。

共识度=S共识S全部×100%\displaystyle 共识度 = \frac{S_{\text{共识}}}{S_{\text{全部}}} \times 100\%
共识度衡量团队对齐程度:三方确认过的场景数除以本期需求总数。BDD 的追求,就是让这个比值逼近 100%。
  1. S\_共识——三方确认的场景数业务、开发、测试都点头说“对,这就是验收标准”的场景。
    S共识: 三方确认的验收场景S_{\text{共识}}:\ \text{三方确认的验收场景}
  2. S\_全部——本期需求总数本期计划交付的全部需求条目。
    S全部: 本期需求总数S_{\text{全部}}:\ \text{本期需求总数}
  3. 乘以 100% 转为百分比理想情况接近 100%,代表需求理解完全对齐。
    ×100%⇒对齐百分比\times 100\% \Rightarrow \text{对齐百分比}

AI 应用场景

场景一:智能客服意图识别模型的 BDD 验收

阶段:需求分析 + 模型验收

解决的问题:产品经理说“要能识别用户投诉”,算法工程师做了一版模型,产品一测发现“卡冻结了”没触发投诉意图。传统做法是反复试错、来回踢皮球,双方对“识别成功”的定义始终不一致。

具体案例:某银行客服机器人团队引入 BDD,用 Given-When-Then 写场景:“Given 用户输入'我的卡被冻结了',When 调用意图识别接口,Then 返回 card_freeze 且置信度 ≥ 0.7”。这条场景放进自动化流水线,模型每次更新都要通过全部场景才允许上线。产品经理和算法工程师用同一语言对话,验收从“我觉得行”变成“场景说了算”。

场景二:模型推理服务用 TDD 保护预处理逻辑

阶段:编码实现

解决的问题:模型推理前要 tokenize、截断、padding、归一化,这些格式转换代码改动频繁,回归风险高。一次不小心改了截断长度,线上推理全部报错,传统靠人工回归效率太低。

具体案例:用 TDD 先写测试:“输入'你好世界',分词后长度应为 4;超过 512 字应截断到 512;空字符串应抛出异常”,再实现分词与截断函数。每次调整 prompt 模板,测试立即守护行为不变。这就是把“验收清单”写在代码前面,每一行预处理逻辑都有据可查。

场景三:机器学习数据流水线的测试金字塔

阶段:质量保障

解决的问题:特征工程代码改一处,下游全挂。传统靠手工跑完整管道验证,一次 20 分钟,团队为了省时间常常跳过验证,带病上线。

具体案例:单元测试覆盖每个特征转换函数(异常值过滤、缺失值填充),集成测试验证特征拼接模块输出的 schema,端到端测试只保留一条“原始数据 → 训练集 → 模型指标”的冒烟测试。金字塔让每次改动在 1 分钟内得到反馈,把数据管道的“砖块、墙面、整楼”都纳入自动化验收。

Python 演示

python84 行
# -*- coding: utf-8 -*-"""TDD/BDD 演示: 购物车折扣计算器""" class ShoppingCart:    """购物车: 纯 Python 演示 TDD 产物"""     def __init__(self):        self.items = {}  # 商品名 -> 价格, 数量     def add_item(self, name, price, qty=1):        if qty <= 0 or price < 0:            raise ValueError("数量和价格必须为正")        self.items[name] = {"price": price, "qty": qty}     def subtotal(self):        """小计: 所有商品原价之和"""        return sum(v["price"] * v["qty"] for v in self.items.values())     def discount(self):        """折扣规则: 满 200 减 20, 满 500 减 80"""        total = self.subtotal()        if total >= 500:            return 80        if total >= 200:            return 20        return 0     def total(self):        """应付金额 = 小计 - 折扣"""        return self.subtotal() - self.discount()  class TestShoppingCart:    """BDD 风格测试: 用场景描述行为"""     def test_empty_cart_total_zero(self):        # Given 一个空的购物车        cart = ShoppingCart()        # When 计算总价        total = cart.total()        # Then 总价应为 0        assert total == 0     def test_discount_when_subtotal_200(self):        # Given 购物车有两件单价 120 的商品        cart = ShoppingCart()        cart.add_item("书", 120, 2)        # When 计算总价        total = cart.total()        # Then 小计 240, 满 200 减 20, 应付 220        assert total == 220     def test_discount_when_subtotal_500(self):        # Given 购物车里有一件 600 元的商品        cart = ShoppingCart()        cart.add_item("显示器", 600, 1)        # When 计算总价        total = cart.total()        # Then 小计 600, 满 500 减 80, 应付 520        assert total == 520  def run_tests():    """模拟 TDD 红绿循环: 运行所有 test_ 开头的场景"""    passed = 0    failed = 0    tester = TestShoppingCart()    for name in dir(tester):        if name.startswith("test_"):            try:                getattr(tester, name)()                print("PASS:", name)                passed += 1            except AssertionError as e:                print("FAIL:", name, "->", e)                failed += 1    print("=" * 30)    print("通过 {} 个, 失败 {} 个".format(passed, failed))    return failed == 0  if __name__ == "__main__":    ok = run_tests()    raise SystemExit(0 if ok else 1)
目的:用购物车折扣计算器演示 TDD/BDD 的产物——测试以 Given-When-Then 场景书写,运行时模拟红绿循环:失败打印 FAIL(红灯),通过打印 PASS(绿灯),最终以退出码通知 CI。
  1. self.items[name] = {"price": price, "qty": qty}用字典保存商品明细,是后续小计和折扣计算的数据基础。
  2. def discount(self):折扣规则是业务核心,TDD 的第一步就是先把这条规则变成测试。
  3. cart.add_item("书", 120, 2)Given 前置:向购物车加入两件商品。
  4. total = cart.total()When 动作:触发总价计算。
  5. assert total == 220Then 断言:期望结果为 220,与手算小计减折扣一致。
  6. for name in dir(tester):用反射收集所有 test_ 开头的测试方法,免去手工登记。
  7. getattr(tester, name)()动态调用每一个测试场景,模拟测试框架的收集与执行。
  8. print("PASS:", name)模拟绿灯输出,说明该场景验证通过。
  9. raise SystemExit(0 if ok else 1)返回进程退出码,CI 系统据此判断本次构建是否通过。

常见误区

误区我以为 TDD 只是时间顺序上的变化,只要先写测试再写代码,就算用了 TDD。于是我把所有测试一次性写完,然后一股脑实现全部功能。

正解TDD 的关键不是“顺序”,而是“节奏”——红-绿-重构的完整循环,且每个循环只推进一小步。先写一个测试、看它失败、写最小代码让它通过、重构,整个过程几分钟一轮。如果跳过红灯阶段,或者从不重构,就退化成“补测试”。就像盖房,不是把整本验收清单贴在墙上就完事,而是每砌一段墙就立刻对照一条清单核验。

为何常见:“Test-Driven“这个字眼太有误导性,让人把注意力全放在”谁先谁后”上,忽略了真正的核心是快速反馈的小步迭代。

误区我看了 Cucumber 的 .feature 文件,觉得 BDD 不过就是用英文句子代替断言而已,没什么新鲜的。

正解BDD 的产出物确实是自然语言场景,但它的核心价值是“三方协作”——业务方、开发、测试在共同编写场景的对话中达成共识。如果只是一个人把已有测试翻译成自然语言,团队之间没有发生对齐,BDD 的精髓就丢了。场景文件是共识的结晶,而不是共识的替代品。

为何常见:工具(Cucumber 等)太显眼,而协作过程是软性的、难以量化的,初学者很容易只看到表面形式。

误区我追求 100% 的单元测试覆盖率,每个方法都配三五个测试,以为这样质量就万无一失。

正解测试金字塔强调分层配比约 7:2:1,单测过多、集成和端到端测试缺失,会漏掉模块间协作的问题——墙砖都是好的,但墙和梁的接口没对齐,整栋楼照样塌。而且过度琐碎的单测会拖慢重构,让代码难以演进。数量不是目标,分层合理才是。

为何常见:“数量等于质量”的直觉,加上覆盖率工具不断给出正向数字反馈,容易让人迷失在局部最优里。

误区我的覆盖率已经 100% 了,所以代码肯定没有 bug,可以放心上线。

正解覆盖率只度量“被执行过”,不度量“被断言过”。一行代码被测试摸了一下但没验证任何输出,覆盖率照样算 100%。真正的质量要看断言的有效性、异常路径的覆盖、边界条件的验证。覆盖率是必要不充分指标——它像一张考勤表,只能证明人到场了,不能证明人干了活。

为何常见:覆盖率是数字化指标,人类对数字有天然的信任感,而“断言质量”难以量化,于是被无视。

误区为了让测试变快、隔离外部环境,我把所有外部依赖都 mock 成假对象,一个真东西都不留。

正解过度 mock 会让测试与真实行为脱节——测试里一切正常,一接真实数据库就炸。正确做法是:优先用真实轻量实现(如内存数据库),只有在无法避免(如第三方支付接口)时才用替身,并警惕 mock 的总量暴涨。测试的价值在于“模拟真实”,而不是“远离真实”。

为何常见:Mock 带来的速度提升和隔离效果立竿见影,像止痛药一样让人上瘾,但病根(过度耦合)反而被隐藏了。

面试题

Q1. TDD 的“红-绿-重构”循环中,“红”指的是什么?

单选题 · easy

  • A.代码运行时报错崩溃
  • B.新写的测试运行失败← 正确答案
  • C.代码风格审查不通过
  • D.部署流水线构建失败

答案:B。红灯 = 新增测试运行失败,证明测试确实在检验新功能。A 是运行时崩溃、C 是规范问题、D 是部署问题,都不是 TDD 中“红”的定义。就像验收水平尺第一次靠上去,必须显示“不合格”才说明尺子真的在量。

Q2. 在 TDD 的绿灯阶段,正确的做法是?

单选题 · easy

  • A.用最简代码让测试通过← 正确答案
  • B.立刻重构优化代码结构
  • C.删除失败测试提高速度
  • D.一次性添加大量新测试

答案:A。绿灯阶段只求“最快让测试变绿”,不追求优雅,重构是下一阶段的事。B 混淆了重构阶段;C 删测试等于自毁清单;D 是红灯阶段的事。先通后优,是 TDD 的顺序铁律。

Q3. BDD 中用户故事的标准“三段式”是?

单选题 · mid

  • A.作为角色,我想,以便← 正确答案
  • B.背景,场景,结果描述
  • C.输入,处理,输出结果
  • D.需求,设计,实现落地

答案:A。用户故事格式是“As a… I want… So that…“,即”作为某角色,我想做某事,以便获得价值”。B 像测试用例描述,C 是数据处理流程,D 是开发流程,都不是用户故事的标准句式。

Q4. 在 Given-When-Then 结构中,Then 部分描述的是?

单选题 · mid

  • A.系统初始状态准备
  • B.用户触发某个动作
  • C.期望的可观察结果← 正确答案
  • D.测试框架的配置项

答案:C。Then 是断言期望结果,比如“应付款显示 220 元”。A 是 Given 的职责,B 是 When 的职责,D 与场景描述无关。三段的顺序不可颠倒,语义各有分工。

Q5. 关于测试金字塔,下列说法正确的有哪些?

多选题 · mid

  • A.单元测试数量最多← 正确答案
  • B.端到端测试最慢← 正确答案
  • C.集成测试成本最低
  • D.单元测试运行最快← 正确答案

答案:ABD。金字塔塔底单元测试数量最多、运行最快、成本最低;塔尖端到端测试最慢最贵。C 说集成测试成本最低是错的,成本最低的是单元测试。比例约 7:2:1,正是为平衡速度与成本。

Q6. 依赖注入的主要目的是?

单选题 · mid

  • A.提升代码运行速度
  • B.让依赖可被测试替换← 正确答案
  • C.减少代码文件数量
  • D.让程序自动生成文档

答案:B。依赖注入把依赖从“内部 new”改为“外部传入”,测试时就能换成替身,隔离数据库、网络等不稳定因素。A、C、D 与依赖注入的动机无关。它解决的是可测试性问题,而不是性能或代码量问题。

Q7. BDD“三剑客”协作中的角色包括?

多选题 · mid

  • A.业务人员← 正确答案
  • B.开发人员← 正确答案
  • C.运维人员
  • D.测试人员← 正确答案

答案:ABD。BDD 三剑客 = 业务方(Product Owner)+ 开发 + 测试,三方共同编写 Given-When-Then 场景。运维人员通常不参与需求场景的定义,除非涉及部署行为本身。

Q8. 覆盖率公式 C = L_covered / L_total × 100% 中,L_total 的含义是?

单选题 · hard

  • A.测试用例的总数量
  • B.可执行代码总行数← 正确答案
  • C.被测功能模块的个数
  • D.全部源文件的字节数

答案:B。覆盖率度量“代码行”而非“测试数量”或“功能数”,L_total 是可执行语句的总行数(不含空行和注释)。A 是测试规模指标,C 是功能点指标,D 是存储量指标,都与行覆盖率定义无关。

Q9. 关于覆盖率与质量的关系,哪些说法是合理的?

多选题 · hard

  • A.覆盖率衡量执行而非断言← 正确答案
  • B.覆盖率 100% 代表无缺陷
  • C.覆盖率是必要不充分指标← 正确答案
  • D.覆盖率越高测试一定越好

答案:AC。覆盖率只证明代码“被跑到”,不证明“被断言”,所以是必要不充分指标;100% 也不能保证无缺陷,可能来自无效断言。B 把覆盖率神化,D 忽略断言质量,都是常见误区。

Q10. 关于 TDD 与 BDD 的关系,正确的理解有哪些?

多选题 · extreme

  • A.BDD 是 TDD 的延伸细化← 正确答案
  • B.TDD 要求测试先行编写← 正确答案
  • C.BDD 完全不写自动化测试
  • D.两者结合使用效果最佳← 正确答案

答案:ABD。BDD 在 TDD 基础上聚焦“行为”,用自然语言让业务方参与,是 TDD 向需求侧的延伸;TDD 的测试先行是硬性要求;BDD 场景最终也要落地为自动化测试,C 错误。实践中常用 BDD 做需求分析和验收,TDD 做编码驱动,两者互补。