BDD 行为驱动开发
BDD 的 Given-When-Then 与 Gherkin、Cucumber 落地方式、与 TDD/E2E 的衔接,以及「BDD 是沟通工具而非测试工具」的心智模型与常见误用。
知识点地图
- 知识类别:BDD 行为驱动开发(测试方法学家族,与 TDD 并列又互补)。
- 解决什么问题:TDD 的测试代码只有开发者看得懂,产品与业务方无法确认「测的就是我要的行为」——BDD 用业务各方都能读的自然语言场景描述行为,把验收标准变成可执行的规约。
- 什么时候用到:需求评审要产出「各方都认账」的验收标准时;验收测试(UAT)要自动化时;E2E 用例需要一个业务可读的规约语言时。
- 同族文档:TDD 的红-绿-重构循环见《测试驱动开发》;BDD 场景落到哪一层执行见《E2E 端到端测试》与《测试层级》。
前置知识:单元测试与 AAA 模式、读一遍《测试驱动开发》第 1 节(本文多处对照 TDD)。不需要先学 Gherkin——本文从心智模型讲起。
一、心智模型:BDD 不是测试工具,是沟通工具
这是理解 BDD 的第一块基石,先于任何语法:
- BDD(Behavior-Driven Development)解决的问题不是「怎么把测试写得更好」,而是「产品、测试、开发三方对同一句话的理解不一致」。需求里写「正常情况下用户可以登录」,三方各自脑中的「正常」是三个版本,直到联调才对齐。
- BDD 的做法:用结构化自然语言(Given-When-Then)把行为写成场景,三方在需求阶段共同确认场景,场景即验收标准;工具(Cucumber 等)再把场景「粘」到自动化代码上,让验收标准可以反复执行。
- 推论:如果一个团队只有测试工程师在写 Gherkin,产品从不参与场景评审,那这个团队只是在用更啰嗦的语法写自动化用例,没有获得 BDD 的本体价值。
三个场景例子说明这个心智模型:
- 真实工程场景(需求歧义):产品说「优惠券可与满减叠加」,开发实现成可叠加、测试按「二选一」写用例——上线前一周才发现。若三方曾共同确认一条「当用户同时持有优惠券与满减资格,那么优惠金额按原价分别计算后取大」的场景,歧义在评审桌上就消失了。
- 回归保护:半年后有人改了优惠逻辑,CI 里那批 Gherkin 场景仍在验证「三方当初认可的行为」——场景是行为规约的长期载体,不是一次性的测试脚本。
- 新人入职:新测试工程师读
features/目录就能理解系统全部业务规则——Gherkin 文件本身就是可执行的业务文档。
二、Given-When-Then 三段式
- Given(给定):前置状态——世界现在是什么样;
- When(当):触发动作——发生了什么操作或事件;
- Then(那么):可观察结果——系统应该变成什么样。
这套结构可以描述任何测试(包括单元测试),也是测试命名的好模板。判断一条场景写得好不好,看三点:Given 是否只讲状态(不讲操作)、When 是否只讲一个动作、Then 是否可观察(而非「系统应正确工作」这种无法断言的表述)。
三、Gherkin 与 Cucumber
Gherkin 是 BDD 场景的结构化自然语言(支持中文关键词 假如/当/那么),Cucumber 是执行它的代表工具(另有 Python 的 behave、.NET 的 Reqnroll 等对应物)。
# features/discount.feature
功能: 订单满减折扣
场景: 满 500 元享受大额优惠
假如 购物车中有 600 元的商品
当 用户提交订单
那么 系统应减免 150 元
并且 订单应付 450 元// steps/discount.steps.ts —— 把每句自然语言粘到代码
import { Given, When, Then } from '@cucumber/cucumber';
import { strict as assert } from 'node:assert';
import { discount } from '../../src/discount';
let cartAmount: number;
let saved: number;
Given('购物车中有 {int} 元的商品', (amount: number) => {
cartAmount = amount;
});
When('用户提交订单', () => {
saved = discount(cartAmount);
});
Then('系统应减免 {int} 元', (expected: number) => {
assert.equal(saved, expected);
});逐段拆解这套机制的运作方式:
- 步骤定义靠正则/表达式匹配粘合:
{int}是参数占位,Gherkin 文本变了(比如「有 600 元商品」改成「购买 600 元商品」)而步骤没同步,Cucumber 会报「步骤未定义」而不是测试失败——这是 BDD 工具里最常见的「看似红实为断线」状态,看到 undefined step 先查文案漂移。 let cartAmount这类跨步骤变量藏在模块级作用域里:一个场景内共享、场景之间必须干净(Cucumber 每个场景重建运行环境;自己写胶水代码时要保证这一点,否则场景顺序影响结果)。- 场景文本是给业务方读的,步骤代码是给 CI 跑的——改文案不改语义时要同步改步骤定义的正则,两边任何一边漂移都会让规约失效。
四、BDD 与 TDD 的关系
| 维度 | TDD | BDD |
|---|---|---|
| 驱动者 | 开发者 | 产品/测试/开发三方 |
| 语言 | 代码 | Gherkin 等结构化自然语言 |
| 关注点 | 单元行为与设计 | 业务行为与验收标准 |
| 产物 | 单元测试套件 | 可执行的验收场景 |
| 循环节奏 | 红-绿-重构(分钟级) | 讨论澄清-场景化-自动化 |
两者不冲突:常见组合是「外层少量 BDD 验收场景 + 内层大量 TDD 单元循环」,对应测试金字塔「少 E2E、多单元」的形状。可以把 BDD 场景理解为 TDD 测试清单的「业务方可见版」——《测试驱动开发》第 1.5 节的测试清单动手前列,BDD 场景则是与业务方共同列的那份。
五、BDD 与 E2E 的衔接
BDD 场景描述的「用户提交订单」若落到真实浏览器/真实环境执行,它就是一条 E2E 用例的规约语言:
- 场景即 E2E 用例的规约:
features/描述「什么行为、什么结果」,步骤代码里调用 Playwright/Selenium 完成真实操作——业务方读场景,CI 跑实现,两层各取所需。 - 放哪一层要想清楚:同一个满减场景,可以粘到纯函数
discount()(快、稳、真实),也可以粘到 Playwright 走完整下单页(真实、慢、flaky)。默认粘到最靠近业务的纯逻辑层,只把少数关键路径粘到真 E2E——场景数量与层级选择同样遵循金字塔。 - 挂进 CI 的工程化细节(分层、并行、门禁)见《CI/CD 测试门禁》。
六、落地建议与常见误用
- 场景应表述业务规则而非页面操作。「当用户提交订单」是规则,「当用户点击 id=submit 的按钮」是把 UI 细节泄漏进需求——UI 一改版,业务规则文档跟着作废。
- 场景数量克制:每个功能 5-10 个关键场景,覆盖主要分支与失败路径;试图用 Gherkin 描述一切会让维护成本失控。
- 反模式「Cucumber 浮肿」:一层 Gherkin + 一层胶水代码 + 一层页面对象,三层转发只为测一个函数。简单逻辑直接 TDD 即可,BDD 只用在需要业务方确认的验收场景。
- 反模式「无三方评审的 BDD」:Gherkin 由测试单人闭门编写,产品从不读——此时 BDD 的沟通价值归零,只剩语法开销。判断标准:最近一次产品在场景上提出修改意见是什么时候。
动手实践
任务:为「购物车优惠券」功能写一份 BDD 场景集(3 到 5 条),并为其中一条配上 Cucumber 步骤代码。
提示:
- 场景写业务规则(叠加规则、门槛计算、过期券行为),不写按钮与页面;
- 每条场景检查三段式纪律:Given 只讲状态、When 只讲一个动作、Then 可观察;
- 步骤代码里的参数用
{int}等占位符,让一条步骤定义服务多条场景。
参考实现(先自己写,再对照):
功能: 优惠券与满减
场景: 满 500 减 150,无优惠券
假如 购物车中有 600 元的商品
当 用户提交订单
那么 系统应减免 150 元
场景: 50 元优惠券叠加满减,优惠取更大者
假如 购物车中有 600 元的商品
并且 用户持有一张 50 元优惠券
当 用户提交订单
那么 系统应减免 150 元
并且 优惠券不消耗// 第二条场景的增量步骤
Given('用户持有一张 {int} 元优惠券', (amount: number) => {
coupon = amount;
});
Then('优惠券不消耗', () => {
assert.ok(coupon > 0); // 断言副作用的「不发生」,与《测试替身》的行为验证呼应
});对照要点:场景标题直接是业务规则复述;「优惠券不消耗」断言的是「没有发生的副作用」,这正是行为验证的典型场景——用 Spy/状态断言皆可,见《测试替身》。
坑点与自检
| 坑 | 现象 | 对策 |
|---|---|---|
| 场景写页面操作 | UI 改版场景全废 | 写业务规则,操作细节留在步骤代码 |
| 三层浮肿 | Gherkin + 胶水 + 页面对象测一个函数 | 简单逻辑直接 TDD,BDD 只做验收场景 |
| 步骤未定义当测试失败处理 | 文案漂移被误判为功能 bug | undefined step 先查场景与定义的文案差异 |
| 无三方评审 | BDD 退化为啰嗦的自动化 | 产品必须参与场景确认,否则不上 Gherkin |
自检清单:
- 每条场景能通过「Given 状态 / When 一个动作 / Then 可观察」三查
- 场景文本里没有出现选择器、按钮、页面 URL 等 UI 细节
- 场景数量与功能复杂度相称(核心功能 5-10 条)
- 知道自己团队 BDD 的驱动者是谁(三方评审还是测试单人)
练习
- 找一个你们团队最近的需求争议(或历史缺陷),把它重写成两条 BDD 场景,检验争议点是否在场景文本里就暴露了。
- 把《测试驱动开发》第 1.4 节订单折扣的测试清单改写成 Gherkin 场景集,对照两种形式各自的读者是谁。
- 给本文第 1 节的优惠叠加规则补一条「不叠加」场景并实现步骤代码,故意把场景文案改一个字,观察 Cucumber 报 undefined step 的过程。
下一步
- 红-绿-重构循环与三定律:见《测试驱动开发》。
- 场景落成真实浏览器操作的工程化:见《E2E 端到端测试》。
- 验收场景挂进门禁:见《CI/CD 测试门禁》。