BDD(行为驱动开发)入门:把“测试”写成“行为”,把“需求”写成“场景”

BDD(行为驱动开发)入门:把"测试"写成"行为",把"需求"写成"场景"

缩写科普(先把字面意思说清楚)

BDD = Behaviour/Behavior-Driven Development(行为驱动开发,英式/美式拼写都常见)

  • B(Behaviour/Behavior,行为):关注"系统应该怎么表现"(可观察的输出/效果),而不是"测了哪些函数"。
  • D(Driven,驱动):用这些行为描述反向驱动后续工作(先写清楚边界与验收口径,再写实现/自动验证)。
  • D(Development,开发):最终目标仍是交付可运行的软件,只是把对齐工作前移。
    TDD = Test-Driven Development(测试驱动开发)
  • T(Test,测试):把测试用例当作驱动载体。
  • D(Driven,驱动):典型节奏是"先写一个会失败的测试 -> 写最少实现让它通过 -> 重构"。
  • D(Development,开发):同样是交付软件,但更强调用测试推动设计与实现演进。

摘要(先看结论)

如果你只记住三句话,就够用:

  1. BDD 想解决的不是"怎么写更多测试",而是"怎么把团队对系统行为的理解写清楚",并让它能自动验证。
  2. 从代码层面开始的一个小招式是:把测试名写成句子,尤其用 should ... 句式,它会逼你把"当前这个类/模块应该做什么"说清楚。
    • 正例:OrderService should 库存不足时拒绝下单并给出原因(一句话就能读懂承诺)。
    • 反例:OrderController should 校验地址并计算优惠并预占库存并调用支付...(一句话塞太多事,通常职责放错了)。
    • 判断规则:如果你很难把测试名写成"当前组件 should ...",先别怪文笔,优先怀疑"行为归属不清/边界没切开",考虑拆分组件或把测试搬家。
  3. 把需求写成 Given / When / Then 的场景(验收标准),再把场景拆成可复用的片段,就能把"需求 → 自动验收 → 代码实现"连成一条线。

快速导航(按你的困惑跳读)

  1. "我看完还是不知道 BDD 是啥":先看「BDD 到底改变了什么」
  2. "should 到底有啥用":看「把测试名写成句子」和「失败时三种解释」
  3. "Given/When/Then 是啥关系":看「把需求写成场景」
  4. "怎么变成可执行的":看「让验收标准可执行:把场景拆成片段」

先画两张图(建立直觉)

下面这两张图就是全文主线。

  1. 从"想要什么"到"能自动验证"的闭环:
text 复制代码
用户故事(想要/价值)
   |
   v
验收标准(场景:Given/When/Then)
   |
   v
场景片段(Given / When / Then 的可复用步骤)
   |
   v
可执行代码(真实对象 or mock)  --> 运行得到反馈
  1. 从"测试思维"切换到"行为思维"的变化:
text 复制代码
以前:我在写测试(test) -> 关注"测哪里、测多少、怎么命名"
现在:我在描述行为(behaviour) -> 关注"系统应该怎么表现"

测试失败时:
  - 我引入了 bug
  - 行为迁移到别处了(测试应该搬家)
  - 行为不再正确(测试应该删掉)

BDD 到底改变了什么

很多人学 TDD 会卡在一组典型问题上:从哪里开始?该测什么不该测什么?一次写多少?名字怎么取?失败了怎么判断是代码错还是假设错?

BDD 的切入点非常朴素:先把你关心的"系统行为"用一句话讲清楚,再让这句话变成可执行的验收标准。换句话说:

  • "test" 容易让人把注意力放在测试本身(数量、覆盖率、写法)。
  • "behaviour" 更容易让人把注意力放在行为本身(系统应该如何表现)。

这不是文字游戏,而是一个写作约束:当你被迫把方法名写成一句话时,你会更容易发现"这个行为到底属于谁"。

把测试名写成句子(尤其是 should)

一个非常小、但很有效的实践是:把测试方法名当作一句能读懂的人话。比如在"电商下单"场景里,把一组测试名渲染成:

text 复制代码
下单(OrderService)
- should 计算订单应付金额(含优惠券/运费)
- should 库存不足时拒绝下单并给出原因
- should 支付超时后自动取消并释放库存

你会立刻得到两个收益:

  1. 失败时更好读:CI 红了,你先看到的是"应该怎么表现",而不是一串工具/实现细节。
  2. 更像"文档":把一组行为列出来,别人一眼就知道这个模块承诺了什么。

接着再加一个更强的约束:用 should 开头。

The class should do something 的隐含规则是:你只能描述"当前这个类/模块的行为"。如果你发现一个测试名很难写进这个句式里,往往不是你文笔差,而是"职责可能放错地方了"。

一个典型例子是:你把"风控校验""地址合规校验""优惠分摊""库存预占"等逻辑都塞进了 OrderController,很快就会写出这种别扭的测试名:

OrderController should 校验地址并计算优惠并预占库存并调用支付并记录埋点...

这通常暗示:这些行为并不"属于 Controller",它更像是被迫承担了太多职责。更合理的拆法是把行为迁到更内聚的组件里(例如 PricingService / InventoryReservation / RiskCheckPolicy),让每个组件的 should 测试都能写成一句清晰的人话。

此时 OrderController 的测试应当收敛到它真正的职责:例如"它会调用下单用例并正确映射错误码",而不是把"优惠怎么算/库存怎么占/风控怎么判"全部塞进同一个类的测试里。

测试失败时,先问一句:它真的 should 吗?

当你用 should 命名时,它还有一个微妙但关键的好处:你可以反问自己一句:

"它真的应该这样吗?"

这句反问会把你从"测试失败了我该修测试/修代码"的焦虑里拉出来,回到更本质的判断:你到底碰到了哪一种情况?

  1. 我引入了 bug:行为仍然正确,但实现错了。
  2. 行为迁移了:行为仍然正确,但它现在由别的组件承担,测试应该搬家(顺便重命名/调整)。
  3. 行为不再正确:系统前提变了,这条行为不再成立,测试应该删掉。

这第三条很重要:删掉不再正确的测试不是"变差",而是"前提更新"。

从"代码行为"走到"需求行为":把需求写成场景

当你把注意力放到"行为"上,你会自然发现:需求本质上也是行为。

一个常见的用户故事模板是:

text 复制代码
作为一名 [X]
我想要 [Y]
以便 [Z]

这个模板的价值在于:它逼你把"价值"写出来(Z)。写不出价值时,需求往往只是"我就是想要"。

接下来把"这个需求什么时候算交付了?"回答成验收标准。BDD 常用的写法是用场景(Scenario)表示,并用 Given / When / Then 描述:

text 复制代码
Given  某些前置条件(上下文)
When   某个事件发生
Then   应该出现哪些结果

你可以把它当作一种结构化叙事:先交代背景,再发生动作,再给出可检查的结果。

一个具体例子:ATM 取现(两条场景)

用户故事(要的是"价值"):

text 复制代码
标题:客户取现金

作为一名客户,
我想从 ATM 取现金,
这样我就不必在银行排队等候。

场景 1:账户有余额

text 复制代码
Scenario: 账户有余额

Given 账户有余额
And 卡有效
And 出钞机中有现金
When 客户请求取现
Then 确保账户被扣款
And 确保吐出现金
And 确保退回银行卡

场景 2:账户透支且超过透支额度

text 复制代码
Scenario: 账户透支且超过透支额度

Given 账户已透支
And 卡有效
When 客户请求取现
Then 确保显示拒绝消息
And 确保不吐出现金
And 确保退回银行卡

注意这里的 And:它让多个前置条件、多个结果能自然串起来。同时,两条场景共享了部分片段(比如"卡有效""客户请求取现""退回银行卡"),这为复用打下基础。

让验收标准可执行:把场景拆成片段

到这里为止,你写下的还是"文本"。BDD 的下一步是:把场景拆成更细的、可复用的片段,并让它们能直接对应到代码。

最关键的思想是:Given / When / Then 不是三段文章格式,而是三类可复用的"步骤":

text 复制代码
Given:把系统放进一个已知状态
When :触发一次行为
Then:验证结果(可观察输出)

实现上你可以把这些步骤组织成代码对象(不拘泥具体语言/框架):

  1. 准备一个共享的"世界/上下文"(world),用于存放本场景里用到的对象与状态。
  2. 依次执行 Given,把 world 填成"已知状态"。
  3. 执行 When,让事件在 world 中发生。
  4. 执行 Then,对 world 的可观察结果做检查。

这样做有两个工程收益:

  1. 片段可复用:同一个 Given/Then 可以在很多场景里复用,避免每条场景都从零写一套脚本。
  2. 从 mock 到真实对象的迁移更平滑:早期你可能用 mock 快速把状态搭出来;随着实现推进,你把这些步骤逐步替换成真实对象与真实交互,最后自然演化成端到端的功能验证。

一句话收口

BDD 的核心不是引入一堆新术语,而是用两种"写作模板"把团队思考固定下来:

  1. should 把代码层的行为说清楚(并在失败时更容易判断 bug/迁移/废弃)。
  2. Given / When / Then 把需求层的行为说清楚(并把验收标准拆成可执行、可复用的片段)。

BDD x OpenSpec:把场景沉淀成可追溯的工程资产

但在真实团队里,还有一个常见断点:场景写出来了,随后就散落在 PRD、会议纪要、聊天记录、测试用例、代码注释里,过一两周就失去"证据链"。

OpenSpec(开源)补的是这一块:它不替代 BDD,而是提供一个把场景/规格/任务/决策放进 Git 并可归档的组织方式,让"行为描述"能长期可维护。

1) 一张对照表:BDD 的写法,落到 OpenSpec 的哪里

你在 BDD 里写的东西 在 OpenSpec 里落到哪里 带来的工程收益
用户故事(作为谁/想要/以便) proposal.md(Intent/Scope/Approach) 需求的价值、边界、取舍能被 Review、能追溯
场景(Given/When/Then) specs/<domain>/spec.md(Delta Spec,写"本次变更新增/修改/删除的行为") 验收标准变成可版本化的"行为契约"
场景拆成可复用步骤(Given/When/Then steps) tasks.md(按步骤拆成可勾选的小任务) 实现顺序与验收点清晰,便于小步合入/回滚
"这条行为属于谁"的归属判断(should 约束) design.md(关键决策与职责边界) 避免决策只存在脑子里;新人能复盘"当时为什么"

2) 一张图:把"场景"串成一条交付闭环

text 复制代码
BDD 的场景(Given/When/Then)
  -> 变成 Delta Spec(可 Review 的行为契约)
  -> 变成 Tasks(可勾选的小步实现)
  -> 变成 Code(按任务逐步落地)
  -> 归档 Change(proposal/design/tasks/specs 一起进历史)

如果你用过 Cucumber/Gherkin,会发现 BDD 的核心写法是一致的:关键不在"是否用某个框架",而在统一团队的行为叙事方式;OpenSpec 的价值是把这套叙事变成"可进 Git 的资产"。

3) 用本文 ATM 场景举个"落到仓库里"的例子(示意)

你在本文里已经写出了验收场景(Given/When/Then)。把它放进 OpenSpec 的一个 change(示意结构):

text 复制代码
openspec/
  changes/
    2026-xx-xx-atm-withdraw/
      proposal.md
      tasks.md
      specs/
        atm/
          spec.md

然后把"账户有余额"这条场景写进 Delta Spec(示意片段,重点是结构而不是格式细节):

markdown 复制代码
## ADDED Requirements

### Requirement: ATM 取现

#### Scenario: 账户有余额
- **GIVEN** 账户有余额,卡有效,出钞机中有现金
- **WHEN**  客户请求取现
- **THEN**  账户被扣款,吐出现金,退回银行卡

这样做的一个现实收益是:当线上反馈"余额足但没吐钞"时,你能在 archive/ 里翻到当时的行为口径、任务拆解和关键决策,而不是在聊天记录里考古。

术语表(第一次看到就查这里)

  1. TDD(测试驱动开发):先写一个会失败的测试,再写最少的实现让它通过,再重构。
  2. BDD(行为驱动开发):把注意力从"测试"转到"行为",让行为描述(包括需求验收)能被自动验证。
  3. should:测试/行为方法命名的句式约束,逼你明确"当前组件的承诺"。
  4. 验收标准(acceptance criteria):判断需求是否交付的条件集合;在 BDD 里常用场景表示。
  5. 场景(scenario):一条具体的可验收情境,通常用 Given/When/Then 描述。
  6. 通用语言(ubiquitous language):团队共享的一套领域词汇,尽量从需求一路贯穿到代码命名。
  7. 依赖注入(dependency injection):把依赖通过构造函数/参数传入,便于替换与测试(例如替换成 mock)。
  8. mock:测试替身,用来快速搭建状态或隔离外部依赖,先把行为跑通。
相关推荐
charlie1145141912 小时前
嵌入式Linux驱动开发(7) 从虚拟设备到真实硬件 —— LED驱动硬件基础
linux·开发语言·驱动开发·内核·c
莎士比亚的文学花园6 小时前
Linux驱动开发(2)——驱动编程
linux·运维·驱动开发
2601_949695597 小时前
开源AI智能体OpenClaw接入DeepSeek V4全流程:从配置到成本
人工智能·驱动开发·ai·电脑
枳实-叶8 小时前
【Linux驱动开发】第二天:内核模块生命周期+内存分配全解
linux·驱动开发
嵌入式小企鹅8 小时前
CPU需求变化、RISC-V安全方案、DeepSeek V4适配、太空算力动态
人工智能·驱动开发·华为·开源·算力·risc-v
智者知已应修善业1 天前
【触发器种类和真值表】2023-7-5
驱动开发·经验分享·笔记·硬件架构·硬件工程
枳实-叶1 天前
【Linux驱动开发】第一天:用户态与内核态通俗讲解+最简字符设备驱动实战
linux·驱动开发·学习
nix.gnehc2 天前
读懂 OpenSpec:AI 编码时代的规范驱动开发新范式
人工智能·驱动开发·sdd·openspec
嵌入式×边缘AI:打怪升级日志2 天前
DS18B20 Linux 驱动开发实战:从时序图到温度读取的保姆级教学
linux·驱动开发