前置知识: 软件工程与测试

BDD 行为驱动开发

9 min入门

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 的本体价值。

三个场景例子说明这个心智模型:

  1. 真实工程场景(需求歧义):产品说「优惠券可与满减叠加」,开发实现成可叠加、测试按「二选一」写用例——上线前一周才发现。若三方曾共同确认一条「当用户同时持有优惠券与满减资格,那么优惠金额按原价分别计算后取大」的场景,歧义在评审桌上就消失了。
  2. 回归保护:半年后有人改了优惠逻辑,CI 里那批 Gherkin 场景仍在验证「三方当初认可的行为」——场景是行为规约的长期载体,不是一次性的测试脚本。
  3. 新人入职:新测试工程师读 features/ 目录就能理解系统全部业务规则——Gherkin 文件本身就是可执行的业务文档。

二、Given-When-Then 三段式

  • Given(给定):前置状态——世界现在是什么样;
  • When(当):触发动作——发生了什么操作或事件;
  • Then(那么):可观察结果——系统应该变成什么样。

这套结构可以描述任何测试(包括单元测试),也是测试命名的好模板。判断一条场景写得好不好,看三点:Given 是否只讲状态(不讲操作)、When 是否只讲一个动作、Then 是否可观察(而非「系统应正确工作」这种无法断言的表述)。

三、Gherkin 与 Cucumber

Gherkin 是 BDD 场景的结构化自然语言(支持中文关键词 假如/当/那么),Cucumber 是执行它的代表工具(另有 Python 的 behave、.NET 的 Reqnroll 等对应物)。

gherkin
# features/discount.feature
功能: 订单满减折扣

  场景: 满 500 元享受大额优惠
    假如 购物车中有 600 元的商品
    当 用户提交订单
    那么 系统应减免 150 元
    并且 订单应付 450 元
typescript
// 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 的关系

维度TDDBDD
驱动者开发者产品/测试/开发三方
语言代码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} 等占位符,让一条步骤定义服务多条场景。

参考实现(先自己写,再对照):

gherkin
功能: 优惠券与满减

  场景: 满 500 减 150,无优惠券
    假如 购物车中有 600 元的商品
    当 用户提交订单
    那么 系统应减免 150 元

  场景: 50 元优惠券叠加满减,优惠取更大者
    假如 购物车中有 600 元的商品
    并且 用户持有一张 50 元优惠券
    当 用户提交订单
    那么 系统应减免 150 元
    并且 优惠券不消耗
typescript
// 第二条场景的增量步骤
Given('用户持有一张 {int} 元优惠券', (amount: number) => {
  coupon = amount;
});

Then('优惠券不消耗', () => {
  assert.ok(coupon > 0);   // 断言副作用的「不发生」,与《测试替身》的行为验证呼应
});

对照要点:场景标题直接是业务规则复述;「优惠券不消耗」断言的是「没有发生的副作用」,这正是行为验证的典型场景——用 Spy/状态断言皆可,见《测试替身》。

坑点与自检

坑现象对策
场景写页面操作UI 改版场景全废写业务规则,操作细节留在步骤代码
三层浮肿Gherkin + 胶水 + 页面对象测一个函数简单逻辑直接 TDD,BDD 只做验收场景
步骤未定义当测试失败处理文案漂移被误判为功能 bugundefined step 先查场景与定义的文案差异
无三方评审BDD 退化为啰嗦的自动化产品必须参与场景确认,否则不上 Gherkin

自检清单:

  • 每条场景能通过「Given 状态 / When 一个动作 / Then 可观察」三查
  • 场景文本里没有出现选择器、按钮、页面 URL 等 UI 细节
  • 场景数量与功能复杂度相称(核心功能 5-10 条)
  • 知道自己团队 BDD 的驱动者是谁(三方评审还是测试单人)

练习

  1. 找一个你们团队最近的需求争议(或历史缺陷),把它重写成两条 BDD 场景,检验争议点是否在场景文本里就暴露了。
  2. 把《测试驱动开发》第 1.4 节订单折扣的测试清单改写成 Gherkin 场景集,对照两种形式各自的读者是谁。
  3. 给本文第 1 节的优惠叠加规则补一条「不叠加」场景并实现步骤代码,故意把场景文案改一个字,观察 Cucumber 报 undefined step 的过程。

下一步

  • 红-绿-重构循环与三定律:见《测试驱动开发》。
  • 场景落成真实浏览器操作的工程化:见《E2E 端到端测试》。
  • 验收场景挂进门禁:见《CI/CD 测试门禁》。