前言
行为驱动开发(Behavior-Driven Development,BDD)常被误解成「一种测试框架」或「写测试的另一种花样」。实际上它首先是一种沟通方式:把需求用接近自然语言的句式写出来,让业务方、产品、开发、测试能一起读懂同一份描述,再让这份描述直接驱动自动化测试。
在 Python 里,BDD 靠第三方库落地,常见的有behave和pytest-bdd,它们都不属于标准库,需要单独安装。本文讲的是思路与骨架,具体的装饰器名、参数与匹配规则以所用框架的官方文档为准——不同版本的细节可能不同,不要照抄记忆里的写法。
文章会覆盖三块:BDD 与 TDD 的定位差别、Gherkin 语法(Given / When / Then)、以及一个behave风格的目录骨架。最后说清它到底适合什么样的团队,不适合的也别硬上。
一、BDD 与 TDD 的定位差异
先把两个词摆正位置。
TDD(Test-Driven Development,测试驱动开发)是开发者内部的工作循环:先写一个会失败的单元测试,再写刚好让它通过的代码,最后重构。它关注的是代码单元的正确性,语言是程序员的语言(函数、类、断言)。
BDD往外扩了一层:它关注的是系统行为是否满足业务期望,语言是业务的语言(用户、场景、期望结果)。BDD 的产物——场景描述——应当能被非技术人员看懂并参与评审。
| 维度 | TDD | BDD |
|---|
| 关注点 | 代码单元正确 | 系统行为符合业务 |
| 语言 | 代码 / 断言 | 自然语言(Gherkin) |
| 参与者 | 开发者 | 业务 + 开发 + 测试 |
| 粒度 | 函数 / 类 | 用户场景 / 端到端行为 |
| 典型工具 | unittest、pytest | behave、pytest-bdd |
| 是否标准库 | 是 | 否(第三方) |
它们不是替代关系。BDD 场景通常还需要底层的单元测试兜底——BDD 负责「做对了事」,TDD/单元测试负责「事做对了」。
二、Gherkin 语法:Given / When / Then
Gherkin 是 BDD 用来写场景的结构化自然语言。它不是英文独有,只是关键词固定。核心结构是一个Feature(功能)下面挂若干Scenario(场景),每个场景由三类步骤组成:
- Given:前置条件,把系统摆到起点状态(「假设用户已登录」);
- When:触发动作,描述用户做了什么(「当他提交订单」);
- Then:期望结果,验证可观察的结果(「那么订单状态应为已支付」)。
配套还有And/But作为Given、When、Then的延续(语义上跟随上一步的类型),以及Background(所有场景共用的前置)、Scenario Outline+Examples(参数化场景)。
Feature: 购物车结算
Background:
Given 用户已登录并且购物车为空
Scenario: 把商品加入购物车
When 用户把「机械键盘」加入购物车
Then 购物车里应有 1 件商品
And 商品名应为「机械键盘」
Scenario Outline: 多件商品累计金额
When 用户把「<name>」以 <price> 元加入购物车
Then 购物车总金额应为 <total> 元
Examples:
| name | price | total |
| 鼠标垫 | 30 | 30 |
| 机械键盘 | 200 | 230 |Scenario Outline用一张参数表把「同一条规则、不同数据」的场景压成一条,避免复制粘贴。这条写法在 Gherkin 里是通用的,无论用哪个框架。
三、目录结构与步骤实现
以behave的常见组织方式为例(具体以该框架官方文档为准):
project/
├── features/
│ ├── cart.feature # Gherkin 场景
│ ├── environment.py # 前后置钩子(可选)
│ └── steps/
│ └── cart_steps.py # 步骤的实现代码
└── src/
└── cart.py每个Feature文件放在features/下;steps/目录里放 Python 代码,把 Gherkin 里的每一句话绑定到一个函数上。绑定靠装饰器配一个匹配模式,函数从步骤文本里取参数,并通过一个共享的context对象在步骤之间传递状态。
# 适用于 Python 3.8+;需要第三方库 behave,装饰器与 API 以官方文档为准
from behave import given, when, then
from src.cart import Cart
@given("用户已登录并且购物车为空")
def step_impl(context):
context.cart = Cart()
@when('用户把"{name}"加入购物车')
def step_impl(context, name):
context.cart.add(name)
@then("购物车里应有 {count:d} 件商品")
def step_impl(context, count):
assert len(context.cart.items) == count
@then('商品名应为"{name}"')
def step_impl(context, name):
assert context.cart.items[-1] == name这里有几个要点:
- 步骤文本与装饰器里的模式必须匹配。花括号
{name}是占位符,默认按字符串解析;{count:d}带类型标记,会转成整数。具体占位符语法以框架文档为准。 context是步骤之间唯一的共享通道:上一步存进去,下一步取出来。不要在步骤函数里用全局变量传状态。- 同一个步骤文本在多个 feature 里可以复用同一份实现,这正是 BDD 省事的地方。
- 步骤实现应当薄,真正的业务逻辑留在
src/里,步骤只做「翻译 + 断言」。
如果用pytest-bdd,思路一样,只是它挂在 pytest 生态里,用 pytest 的 fixture 代替context,并且能复用 pytest 的插件与报告。选哪个取决于团队是否已经在用 pytest。
四、BDD 适合什么样的团队
BDD 不是万能药,它有明确的适用边界。
适合的情况:
- 业务方愿意参与需求评审,能一起看场景、提修改意见;
- 需求以「用户行为」为主,比如电商下单、审批流程、账户操作——这些用 Given/When/Then 描述非常自然;
- 团队需要一个跨角色的共同词汇表,减少「开发理解的需求」和「业务想要的需求」之间的偏差。
不适合硬上的情况:
- 纯算法 / 底层库开发,没什么「用户行为」可描述,写成场景反而绕;
- 只有开发者一个人、没人参与评审——那额外维护一层 Gherkin 只是负担;
- 团队还不具备自动化测试基础设施,先补单元测试的坑更实在。
一句话:BDD 的成本在「维护自然语言与实现的一致性」,收益在「跨角色沟通」。如果沟通本身就是痛点,它划算;如果只是想要个测试跑起来,直接用unittest或 pytest 就好。
常见坑点
- 把 BDD 当成「更高级的测试框架」
❌ 只关心场景能不能跑,不管业务方是否真在看
✅ 把它当沟通工具,场景要能过业务评审
- 步骤文本与实现模式对不上
❌ Gherkin 里写「用户登录」,代码里@given("用户已登录")
✅ 两边逐字对齐,或统一由步骤模板生成
- 把业务逻辑写进步骤函数
❌ 步骤里堆几百行计算
✅ 步骤只做翻译与断言,逻辑留在被测代码里
- 用全局变量在步骤间传状态
❌ 步骤之间靠模块级变量通信
✅ 用框架提供的context/ fixture
- 场景写得过于技术化
❌ 「Then 数据库表 orders 的 status 字段等于 1」
✅ 「Then 订单状态应为已支付」
- 每个测试点都写一条 Scenario
❌ 复制十几条只差数据的场景
✅ 用Scenario Outline+Examples参数化
- 拿 BDD 场景替代单元测试
❌ 所有验证都堆在端到端场景里,慢且难定位
✅ BDD 覆盖主流程,细节逻辑仍由单元测试兜底
- 假设框架 API 通用
❌ 把 behave 的写法直接搬到 pytest-bdd 上
✅ 以所用框架官方文档为准,不混用
总结
| 要素 | 含义 | 落地方式 |
|---|
| Feature | 一个功能 | 一个.feature文件 |
| Scenario | 一个用户场景 | Given / When / Then |
| Scenario Outline | 参数化场景 | 配Examples表格 |
| Step | 场景里的每一步 | 绑定到一个 Python 函数 |
| context | 步骤间共享状态 | 框架提供的对象 |
| 角色 | 业务 + 开发 + 测试 | 共同评审场景 |
BDD 的价值不在语法本身,而在于让「需求」和「验证」用同一份文本表达。如果团队里真的有人会读这份文本,它就能省下大量的沟通成本;如果没有人读,那它只是一层多余的外壳。