AI Agent 参与项目交付之后,第一件让人不适的事,是测试突然变得“太容易了”。你把需求丢给 Agent,它生成一堆断言,跑起来全绿,没有任何报错,然后发布上线,业务却说功能根本不对。这不是个例。Yadda 3.0.0 的标题被写成“BDD in the Age of AI Agents”,看起来是在讲一个老测试库的版本更新,实际上是在提醒一件事:当 AI 能自动生成步骤并执行断言时,BDD 的真正价值不是“把自然语言翻译成测试代码”,而是把业务验收规则变成人和 AI 都能遵守的一份契约。
这也是我读这个版本标题时的第一判断:Yadda 3.0.0 在 AI Agents 时代不是过时,而是重新变得重要。它没有让测试变得“更聪明”,它让测试变得“更可核对”。在接下来的文章里,我会从为什么 BDD 会重新回到舞台中央、Yadda 和 Cucumber 的差异、最小可运行流程、到如何把 AI Agent 放进这条链路,逐步拆开讲。最后一节会给出实际落地前必须检查的边界,以及长期使用时会反复踩到的几个坑。
1. 为什么 AI Agent 接管测试后,反而要重新拾起 BDD
1.1 一个被“全绿”掩盖的问题
很多团队让 AI Agent 参与测试的方式是这样的:把历史功能说明丢给大模型,让它生成一组测试用例,再放到某个框架里跑。表面上流程顺了、报告也全绿,但人和业务之间失去了一个关键环节——确认“这些测试到底验证的是不是真实业务规则”。
我见过最典型的一次:一个订单模块的历史接口有 20 多个测试点,AI 自动生成的测试全部通过,后来发现大多数断言判断的是一个已经改名的基础字段,业务逻辑本身根本没有被覆盖到。原因很简单,大模型在处理自然语言时非常擅长“生成看起来合理的步骤”,但并不天然具备“验收责任”。它能写expect(order.status).to.equal('paid'),却不一定能判断这个断言和产品验收标准是不是同一件事。
BDD 的应对方式,是强制把业务规则写在 feature 文本里,并且让测试步骤从文本里长出来。这个约束在纯人工时代容易被当成流程负担,但在 AI Agent 时代反而成了护栏。
1.2 BDD 的原始契约正在被放大
传统 BDD 的做法是:先用 Given/When/Then 描述一个可验收场景,再写步骤定义,最后执行器把文本变成可运行的测试。过去有人觉得这层文本是“多余的中间层”,因为它增加了代码量,却不能直接提高覆盖率。
但在 AI Agents 时代,这层文本变成了人和 AI 之间唯一的稳定接口。
如果测试只是代码,AI 可以随便改,因为代码对它来说没有语义成本;如果测试必须从 feature 文本推导出来,那就意味着每一步改动都要回到业务语言里去对齐。Yadda 3.0.0 这种基于 Gherkin 风格文本的解释器,本质上就是把“业务验收规则”定义成一种人可以审、机器可以执行、AI 可以理解的通用格式。对 Agent 来说,它不需要猜测业务含义,只需要按文本执行;对人来人来说,审核重点也从“看代码细节”变成了“看业务场景是否合理”。
这才是我认为“BDD in the Age of AI Agents”这个标题想表达的真正含义:不是 BDD 变成了 AI 的附属品,而是 AI 让 BDD 过去那层“看似多余”的文本,重新拥有了契约价值。
1.3 适用边界先讲清楚
不是所有团队都适合立刻转向这个组合。从我接触到的工程实践看,更适合的情况通常是:
- 业务方或产品经理能提供相对明确的功能描述,哪怕只是口头约定。
- 团队愿意维护一份 feature 文件,而不是把测试代码当成唯一事实。
- 希望 AI Agent 帮助生成测试步骤,但还保留人工审核和人工兜底。
不太适合的场景也比较明显:
- 团队已经用纯单元测试覆盖了核心逻辑,且业务语言不稳定,经常改名词。
- 测试环境极不稳定,连最基本的环境准备都做不到可复现。
- 没有人工兜底流程,完全让 Agent 自动生成测试、自动执行、自动上线。
这些边界会在第三章和第五章展开。先记住一句话:BDD 在 AI 时代有用的前提,是人仍然在验收链路上承担责任;如果人完全退出这个链路,那 feature 文本也会退化成一种形式主义。
2. Yadda 3.0.0 不是 Cucumber 的平替,而是另一种执行哲学
2.1 先把它和 Cucumber 做一次对比
很多人一听到 BDD,第一反应是 Cucumber。确实,Cucumber 在生态完整度上更成熟,有 Gherkin 解析、有 CLI、有很多语言的实现。但 Yadda 走的是另一条路:更轻、更少魔法、更容易嵌入到现有测试体系里。它不试图变成“你唯一的测试平台”,而是把一个 BDD 执行器塞进你已有的 Mocha、Jasmine 或自定义 Runner 里。
我做了个对比,方便你快速定位:
| 维度 | Cucumber | Yadda 3.x(常见表现) |
|---|---|---|
| 定位 | 完整 BDD 测试框架 | 轻量 BDD 解释器 / 库 |
| 执行方式 | 自带 CLI 和运行器 | 通常嵌入现有 Runner,自己触发 run |
| 依赖重量 | 较重,生态功能多 | 更轻,更贴近“解释 feature 文本”本身 |
| 学习门槛 | 相对高,概念多 | 相对低,核心就是步骤库 + 实例 |
| 和 AI Agent 协作友好度 | 中上,但要做不少环境接入 | 更高,因为 API 直接,容易写脚本 |
| 适合团队 | 需要完整 BDD 平台、报告、多人协作 | 已经有用例框架,只想补 BDD 层的团队 |
这里要补充一句:如果你的团队已经深度使用 Cucumber,并且报告、命令行、插件都成熟,没必要为了“换”而换。Yadda 的价值不在“比 Cucumber 更好”,而在它给了另一种选择:你只想要一个能把 feature 文本跑起来的轻量层,不想被框架套住。
2.2 Yadda 的执行机制
从这类库的常见机制看,核心对象通常是三个:
localisation/English:负责把 Given/When/Then 这类关键词解析出来。library:步骤定义的集合,相当于“步骤字典”。Yadda.createInstance(library):根据字典创建一个解释器,再把 feature 文本喂给它。
如果用伪代码表示,大概是这个样子:
const Yadda = require('yadda'); const English = Yadda.localisation.English; const library = English.library() .given('a user named "$name"', function (name) { this.user = { name }; }) .when('the user searches "$keyword"', function (keyword) { this.keyword = keyword; }) .then('the page shows a result', function () { // 断言逻辑 }); const yadda = Yadda.createInstance(library); const featureText = ` Feature: 搜索商品 Scenario: 按名称搜索 Given a user named "tester" When the user searches "laptop" Then the page shows a result `; yadda.run(featureText, (err) => { if (err) { console.error('BDD execution failed:', err.message); process.exit(1); } });这个例子只是一个常见的旧版 API 结构,具体到 Yadda 3.0.0,如果 API 有变化,以官方文档为准。
之所以把这一步写出来,是因为它揭示了 Yadda 的核心设计:feature 文本并不是文档注释,它是真正会被解释器执行的脚本。步骤定义里的参数、上下文、断言,都要围绕文本来展开。这决定了后面用 AI Agent 辅助时,我们为什么要先稳定 feature 文本,因为它才是整条链路的输入契约。
2.3 版本升级前先做四件事
从 2.x 升到 3.0.0,或者从别的版本迁移到 3.x,我建议先做这四个检查,而不是直接装最新版。
1. 看 CHANGELOG / Release Notes: 确认是否有 breaking change,特别是 API 导出方式和回调签名。 2. 看包入口: 默认是 CommonJS 还是 ESM? 如果是 ESM,你的测试环境是不是已经支持 ESM,或者需要加构建步骤。 3. 看你原来的步骤定义写法: 如果旧代码里大量依赖 this 传递上下文,确认 3.x 是否保留这种机制, 还是改成了显式传 context 对象。 4. 跑一遍最小 feature: 不要一上来迁移全部用例,先跑通一个最小的 Given/When/Then 场景, 再逐步扩展。这一步不是教条,是因为 BDD 库的 API 迁移经常在一个看似不起眼的地方出问题:比如本地化关键词的导入方式、step 定义的匹配规则、异步回调的判定方式。这些问题不会在文档首页写出来,只有当你跑最小用例时才会暴露。
3. 从零搭建一条最小 BDD 执行链路
3.1 目录结构与安装
先准备一个干净的目录。常见做法是把 feature 文件、步骤定义、执行入口分开,便于后续扩展。
bdd-demo/ ├── features/ │ └── search.feature ├── steps/ │ └── search.steps.js ├── tests/ │ └── run.search.js ├── package.json └── node_modules/安装阶段,我这里假设使用 Yadda 3.x,并且仍然搭配 Mocha 作为测试 Runner:
npm init -y npm install yadda npm install --save-dev mocha如果你的项目里已经有测试框架,比如 Jasmine 或 Node 自带的 test runner,也可以不装 Mocha,因为 Yadda 属于库,本身不强制绑定某个 Runner。这里用 Mocha 只是为了演示一个“把 feature 跑起来”的最小闭环。
3.2 写一份能被 Yadda 解释的 feature
feature 文件的核心不是语法花哨,而是场景足够小、步骤足够明确。
Feature: 搜索商品 Scenario: 按名称搜索 Given a user named "tester" When the user searches "laptop" Then the page shows at least one product这里有几个常见问题需要提前说明:
- 步骤文本要和步骤定义里的文本尽可能一致,因为解释器会拿 feature 里的句子去匹配“步骤字典”。
- 变量部分可以用引号或占位符表达,具体语法和版本有关;如果发现匹配不上,先检查这一步。
- 一个 Scenario 里不需要堆十几个步骤,步骤多了以后,维护成本和定位问题的成本会成倍上升。
在 AI Agent 参与之后,这一步看起来会被弱化,因为 Agent 可以帮你生成 feature。但我仍然建议由人先写一个“锚点场景”,让 Agent 在这个范围内补全,而不是完全放养。原因很简单:如果锚点错误,Agent 生成的整个场景集合都会错。
3.3 定义步骤库
假设我们已经有步骤定义库,一个常见写法是:
// steps/search.steps.js const assert = require('assert'); module.exports = function (library) { library.given('a user named "$name"', function (name) { this.currentUser = { name }; }); library.when('the user searches "$keyword"', function (keyword) { this.keyword = keyword; globalThis.searchResults = mockSearch(keyword); }); library.then('the page shows at least one product', function () { assert.ok(globalThis.searchResults.length > 0, '搜索结果不应为空'); }); }; function mockSearch(keyword) { if (keyword === 'laptop') { return [{ id: 1, name: 'Laptop Pro' }]; } return []; }这里的this是否能作为上下文传递,取决于 Yadda 当前版本的行为。如果你发现this拿不到数据,就改成显式传入一个 context 对象,这是更稳妥的方式。
另外,globalThis这种写法只是为了演示“临时状态”,真实项目中不建议把业务数据挂在全局对象上。它会导致测试之间的污染。
3.4 用 Mocha 串起执行
步骤库定义好之后,需要有一个执行文件把 feature 文本读进来,再交给 Yadda 解释器跑。
// tests/run.search.js const fs = require('fs'); const path = require('path'); const Yadda = require('yadda'); const English = Yadda.localisation.English; const featureText = fs.readFileSync( path.join(__dirname, '../features/search.feature'), 'utf8' ); const library = English.library(); const defineSteps = require('../steps/search.steps'); defineSteps(library); const yadda = Yadda.createInstance(library); describe('search feature', function () { it('should run BDD scenario', function (done) { yadda.run(featureText, done); }); });然后执行:
npx mocha tests/如果一切正常,你会看到测试用例通过,并且日志里能定位到是哪份 feature、哪个步骤。
这里需要说清楚一件事:Yadda 不一定自带像 Cucumber 那样友好的彩色文本报告。它更接近“把 feature 文本跑在一个既有测试框架里”。所以你的报告格式、CI 日志、失败定位方式,往往还要依赖 Mocha 或其他 Runner 的能力。这不算缺点,而是一种取舍。
3.5 快速排查链路
如果跑不通,先别急着怀疑版本。按下面这个顺序查,通常能定位到问题:
1. 现象: 是“无法匹配步骤”,还是“执行时报断言错误”,还是“直接空白通过”? 2. 输入: feature 文件是否被正确读取? 中文/英文标点是否被错误处理? 文件编码是否为 UTF-8? 3. 环境: Node 版本是否在 Yadda 3.x 的支持范围内? 当前是 CommonJS 还是 ESM?依赖有没有装全? 4. 参数: 步骤文本里的引号、空格是否和定义完全一致? 是否有多个步骤定义匹配同一个句子,造成歧义? 5. 工具限制: 是否用了 Yadda 当前版本不支持的语法? 本地化关键词是否正确导入?这里最容易踩坑的一点是步骤匹配。如果步骤定义是'the user searches "$keyword"',而 feature 里写的是the user searches laptop,没有引号,匹配大概率失败。这种问题看起来不起眼,却会浪费大量时间。所以我把这条放在首个排查项的后面。
4. 把 AI Agent 放进 BDD 流程:先起草、再收口、后执行
4.1 AI Agent 的三个入口
引入 AI Agent 以后,它可以在 BDD 流程里承担三类事情:
第一类:整理历史资料。把旧需求文档、接口说明、变更记录转成 feature 文本。这个入口风险最大,因为旧资料本身可能已经过时。它产出的不是最终答案,而是待审素材。
第二类:生成步骤定义。已经有 feature 文本,让 Agent 根据文本里的 Given/When/Then 生成对应步骤代码。这个入口速度很快,但需要人工确认断言条件是否正确,尤其是数字边界、状态判断、返回值结构。
第三类:诊断失败日志。当 Yadda 执行失败,把报错信息、feature 文本和步骤定义一起交给 Agent,让它给出修订建议。这个入口相对安全,因为失败信息已经把问题边界缩小了,Agent 不太容易“跑偏”。
4.2 一个可复用的协作框架:先起草、再收口、后执行
我在实际项目里比较推荐把流程固化成四个阶段,而不是让 AI 一口气完成所有事。
| 阶段 | 谁来做 | 主要产出 | 审核重点 |
|---|---|---|---|
| 起草 | AI Agent | 一份候选 feature 或步骤定义 | 是否真实反映业务规则 |
| 收口 | 人工 | 一份可执行的 feature 文本 | 步骤是否清晰、无歧义、无重复 |
| 执行 | Yadda + Runner | 测试结果与失败信息 | 结果是否符合预期 |
| 修订 | AI Agent + 人工 | 修复建议或新版本文件 | 修订是否引入新的业务偏差 |
这个框架最关键的是第二阶段的“收口”。如果收口没做好,后面 AI 再怎么加速,也只是在错误的方向上加速。
我见过一些团队跳过收口,直接让 Agent 生成 feature 和步骤,然后交给 Yadda 跑。结果是:测试绿了,但业务说“这不是我们要的”。问题出在生成阶段没有任何人站在验收角度去审视文本。把“人工审文本”前置,成本看起来增加了,实际上省掉的是后面返工的时间。
4.3 为什么 Yadda 比“全自动生成测试”更适合这个阶段
原因不复杂。全自动生成测试的流程里,feature 文本不是一个强约束,Agent 可以绕过业务规则直接写断言。而 Yadda 的执行链路是:文本 -> 步骤匹配 -> 断言。只要步骤匹配规则严格,文本本身就成了不可跳过的中间层。
此外,Yadda 本身的依赖更轻、API 更直观,很容易被写进 Agent 的工具脚本里。你可以让 Agent 直接调用一个run-bdd.js脚本,把失败信息回收,再进入下一轮修改。相比之下,如果接的是完整 Cucumber 平台,虽然也能做 Agent 接入,但要处理的 CLI 参数、报告插件和配置项会更多。
不过这里也要划一条边界:Yadda 并不保证 AI 生成的内容安全。它只是把“业务语言”和“执行结果”之间的对应关系拉得足够近。最终验收的责任仍然在人这边。
5. 单次跑通不等于能长期使用:五个容易出问题的地方
5.1 共享状态的“隐形污染”
BDD 测试最容易被忽视的问题,是场景之间的共享状态。拿第三章的例子来说,如果globalThis.searchResults在某个步骤里被改了,下一个 feature 执行时读到的可能就是上次残留的数据。
建议的做法是:每个 Scenario 执行前都初始化 context;所有临时数据都挂在场景对应的 context 上;全局对象只用于模拟外部服务,且执行后必须清理。
5.2 异步和上下文容易出现的超时
Yadda 涉及的步骤经常是异步的,比如请求后端、读取数据库、调用模型接口。如果步骤定义里用了 Promise,但执行器按回调方式等待,就很容易出现“测试已经结束,异步还没完成”的情况。
排查方式也不复杂:
1. 看步骤函数有没有显式 return Promise。 2. 看 Runner 的超时时间是否足够。 3. 看步骤定义的回调签名和当前版本匹配不匹配。 4. 可以在步骤里加一条临时日志,确认执行顺序。如果是和老版本迁移相关的,更不要忽略前面说的版本检查。异步行为往往是框架升级里改动最隐蔽的区域。
5.3 步骤匹配的模糊性
当 feature 数量多起来以后,很容易出现两条步骤文本相似度很高,比如“用户点击搜索”和“用户点击搜索按钮”。如果步骤定义里用了宽泛匹配,Yadda 可能错误匹配到其中一条,导致执行结果不符合预期。
我给的建议是:feature 文本写得尽量接近自然业务语言,但变量部分要固定格式。例如:
Given 用户已登录 When 用户搜索 "$text" Then 页面展示至少一条结果避免:
Given 用户 When 搜索 Then 有结果后者虽然简短,但在多场景叠加时会产生大量歧义。让 Agent 起草时,也要给它明确指令:不要省略业务对象,不要用“当用户操作”这种模糊动词。
5.4 工程化最小配置
如果只是本地验证,按照第三章的流程就够了。但长期使用,至少要补上下面这些能力:
- 日志: 记录每条 feature 的执行时间、步骤名和失败原因。 - 输出: 一个 feature 对应一个输出文件,方便 CI 归档。 - 权限: 测试环境使用的账号、密钥、模型路径不能写死在 feature 里。 - 批量控制: 如果同时跑几百条 feature,不要一开始就并发全开, 先小批量跑,确认没有资源竞争。 - 失败重试策略: 注意,重试会掩盖真实问题。 更推荐的做法是失败后保留现场,而不是立刻重试。这些不是 Yadda 本身的功能,而是用 Yadda 的时候,执行层和 CI 层需要补的配套设施。不要指望一个轻量 BDD 库帮你管住所有工程化问题。
5.5 适用场景总结
用一张表收一下:
| 场景 | 适合程度 | 说明 |
|---|---|---|
| 历史业务规则整理成可验收文本 | 适合 | feature 文本本身就是文档 |
| 单元级或服务级业务逻辑验证 | 适合 | 轻量、好嵌入 |
| 跨端全链路 E2E | 不太适合 | 需要大量环境准备,更适合专门 E2E 框架 |
| 完全无人值守的 AI 自动生成测试 | 不适合 | BDD 的价值前提是人工收口 |
| 超大团队统一测试平台 | 看情况 | 如果已有完整平台,不一定要引入新库 |
6. 给开始尝试的人三条实操建议
6.1 先跑最小的 feature,别一上来接全套框架
不要为了尝试 Yadda 3.0.0 就重构成完整 BDD 平台。先拿一个真实的、边界清晰的小场景,写好 feature,定义三步步骤,跑通一次执行。这个最小闭环会告诉你三件事:当前版本 API 是什么样、步骤匹配规则是否顺畅、和现有测试框架能不能兼容。等这个闭环稳了,再扩展出去。
6.2 让 feature 文本成为改动需要 review 的代码
把 feature 文件放在版本管理里,并像代码一样去 review。尤其是在 AI Agent 参与之后,最需要盯住的就是 feature 文本有没有被悄悄改成“更容易通过”的样子。如果一份 feature 从“用户搜索后看到结果”被改成“用户搜索后不报错”,步骤是绿了,验收价值已经丢了。
6.3 给 AI Agent 设好边界
AI Agent 可以做起草者、代码生成器和失败分析助手,但不要让它成为最终验收者。在流程里明确两件事:凡是 feature 文本的实质性改动,必须有人工确认;凡是 Yadda 跑出的失败结果,必须先看日志再让 Agent 修订,不能直接让它自动改测试来消除失败。
如果只让我留一句话做总结,我会说:Yadda 3.0.0 在 AI Agents 时代真正值得关注的价值,不是它多了一个测试功能,而是它把“业务验收规则”变成了一段人和 AI 都承认的契约文本。技术会迭代,Agent 会变强,但这段契约越清晰,自动化就越不会变成盲目自信的工具。