ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Dify E2E 测试实践:Cucumber Gherkin 场景、步骤定义与参数表达式的最佳实践

Dify E2E 测试实践:Cucumber Gherkin 场景、步骤定义与参数表达式的最佳实践 Dify E2E 测试实践Cucumber Gherkin 场景、步骤定义与参数表达式的最佳实践【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify本文基于 Dify 仓库内置的 E2E 测试技能文档 cucumber-best-practices.md 展开讲解在 Dify 的e2e/目录Cucumber Playwright 组合中编写与评审 Gherkin 场景、步骤定义Step Definitions、参数表达式Cucumber Expressions以及步骤复用时应遵循的方法论。读完后你将掌握如何把场景写成“可执行的产品行为规格”、如何在 Dify 的全局步骤命名空间中做真实而非生硬的复用、如何用{string}等表达式保持 Gherkin 可读性以及标签Tag体系与 World 状态管理的源码级实现依据。背景这份规范在 Dify 测试体系中的位置Dify 的仓库级 E2E 套件位于 e2e/其架构由 e2e/AGENTS.md 定义Cucumber 负责场景与钩子Hook的调度和报告Playwright 提供浏览器自动化、上下文、定位器、断言、请求与追踪 API。配套的仓库级技能文档 SKILL.md 明确了主题路由场景措辞、步骤粒度、表达式、World 状态、钩子可见性或标签设计问题归属本文所讲的 Cucumber 最佳实践参考文档而定位器、断言、隔离与等待决策则归属 playwright-best-practices.md。也就是说本文的规范不是泛泛而谈的 BDD 口号而是直接服务于 Dify 这套真实运行的回归套件默认场景复用共享的已认证存储状态标签驱动选择范围Cucumber 退出码是行为门禁且 runner 要求至少有一条testCaseStarted消息以防止空的标签选择“假通过”。原则一把场景当作可执行规格来写原文档的第一条核心建议是Cucumber 场景应当以声明式方式描述行为而不是复现一段交互脚本。具体做法写“用户做了什么、应当发生什么”避免 UI 内部措辞如选择器细节、DOM 结构或组件名语言要足够具体使场景读起来像“活文档”living documentation。唯一的例外当交互机制本身就是被测行为时过程性措辞是合理的例如键盘导航、焦点移动或必须按顺序执行的多角色multi-actor序列。Dify 套件中确实存在这类场景——browser-smoke标签下的键盘与导航覆盖正是“过程即行为”的典型案例。对照仓库中的真实场景 authenticated-entry.feature可以看到标准写法smoke authenticated Feature: Authenticated console home Scenario: Open the default console entry with the shared authenticated state Given I am signed in as the default E2E admin When I open the default console entry Then I should be on the console home And I should not see the Sign in button整个场景没有任何选择器或 DOM 词汇Given建立初始上下文When是用户动作Then/And描述用户可观察的结果。对应的步骤定义 navigation.steps.ts 内部虽然使用了getByRole等 Playwright 定位手段但这些实现细节全部封装在 glue 层不会泄漏到 Gherkin 文本中——这正是“规格层与自动化层分离”的示范。原则二保持场景聚焦原文档要求一个场景证明一个业务规则或一个连贯的结果。Cucumber 社区“三到五个步骤”的建议只是评审启发式而非硬性上限当场景变长时应检查是否存在多个结果、顺带的 setup、或可以下沉到领域步骤背后的 UI 流程。Gherkin 关键字的分工约定Given初始上下文When触发事件Then期望结果。需要强调的是两个易被误读的点Given/When/Then 阶段的重复不是自动失败而是一个“重新审视叙事”的信号避免隐藏依赖场景之间不得依赖彼此的副作用。此外原文档对Rule和Background给出了明确取舍用Rule包装“多个示例说明同一条具名业务规则”的情形Background只用于读者理解场景所必需的共享上下文不要在里面隐藏 fixture 准备、运行时就绪检查或冗长 setup。Dify 套件对此有配套约束种子脚本seed拥有共享的长生命周期 fixture场景拥有自己创建的临时资源并必须注册清理见 e2e/AGENTS.md 的 “Seeds, Cleanup, And Diagnostics” 一节。这保证了Background里不会出现“偷偷初始化环境”的黑盒fixture 的归属始终可追溯。原则三复用步骤但只在行为真正一致时复用原文档将“坏的复用”概括为一句话好的复用减少重复坏的复用隐藏语义。应当优先复用的情形用户动作确实是同一个动作期望结果确实是同一个结果措辞在不同 feature 中仍然自然参数是真实的产品领域值如具名的界面、模式、资源或状态。应当另写新步骤的情形行为存在实质性差异强行复用旧措辞会让场景产生误导所谓“通用步骤”实际上变成实现细节的包装器。总结论是不要为了压低步骤数量而制造模糊步骤而是优化出一组“真实、由领域拥有”的步骤。Dify 的步骤定义目录结构直接体现了这一条。e2e/features/step-definitions/ 按能力域capability组织apps/、auth/、accessibility/、agent-v2/等common/只留给真正跨能力的步骤。同时有一个关键的运行时事实——所有步骤定义共享同一个全局匹配命名空间与它们所在的目录无关。目录只是代码组织方式不是命名空间隔离。因此步骤定义的组织要按领域能力划分避免与单个 feature 耦合的 glue表达式不能宽到与不相关行为重叠否则全局匹配会发生歧义或冲突。以 create-app.steps.ts 为例I select the {string} app type步骤的参数是“Chatbot / Workflow”这类真实的产品领域值动作和结果在多个 feature 中语义一致属于教科书式的正当复用而I confirm app creation内部包含了等待POST /console/api/apps响应、用zPostAppsResponse做契约校验、并把新建应用 ID 记入 World 状态等实现细节——这些细节被封装在步骤内部Gherkin 层只保留一个连贯的领域动作。原则四优先使用 Cucumber Expressions原文档要求除非正则确有必要否则一律使用 Cucumber Expressions。常用形式表达式适用场景{string}标签、名称、可见文本{int}计数{float}十进制数值{word}仅当值确实是一个不可分割的单词token时两条配套纪律保持表达式可读。如果一个步骤需要复杂的解析逻辑先反问是不是场景措辞本身应该更简单只在它能让 Gherkin 保持可读时才用有界的自然语言正则替代例如原文档给出的例子/(Web app|Backend service API)/要避免接受“无主语言”unowned language的宽正则。Dify 的现有步骤定义大量使用{string}且用法克制。例如 navigation.steps.ts 中的I should see the {string} buttonThen(I should see the {string} button, async function (this: DifyWorld, label: string) { await expect(this.getPage().getByRole(button, { name: label })).toBeVisible() })参数label是按钮的可见文本——一个真实的产品领域值而不是颜色、索引或内部 ID。这类步骤在 smoke、认证、应用管理等多个 feature 中被自然复用措辞在每处都保持诚实恰好落在“原则三”的正当复用区间内。原则五保持步骤定义薄而有意义原文档对步骤定义给出了三层约束步骤定义是 Gherkin 与自动化之间的胶水不是第二套抽象语言。一个步骤应表达一个连贯的领域动作或结果为了让 UI 机制不进入规格层它可以内部调用多个实现辅助函数。状态放在 World 里而不是模块全局变量。Cucumber 会为每个场景创建新的 World 实例因此场景状态必须挂在 World 上以保证场景间隔离。钩子对 feature 读者是不可见的。把钩子留给低层的浏览器生命周期、清理与诊断凡与业务相关的上下文一律表达在 Gherkin 中。DifyWorld每条原则的源码级落地Dify 的 World 实现 是这一原则最完整的证据。DifyWorld继承自cucumber/cucumber的World并通过文件末尾的setWorldConstructor(DifyWorld)注册为全局构造器export class DifyWorld extends World { context: BrowserContext | undefined consoleClient: ConsoleClient | undefined page: Page | undefined consoleErrors: string[] [] pageErrors: string[] [] createdAppIds: string[] [] createdAgentIds: string[] [] scenarioCleanups: ScenarioCleanup[] [] // ... } setWorldConstructor(DifyWorld)几个值得注意的设计点与原文档规范逐条对应每场景新 World 显式状态重置构造函数与startSession都会调用resetScenarioState()把createdAppIds、lastCreatedAppName、agentBuilder等字段归零。这直接落实了“场景状态放 World、不放模块全局”的隔离要求也让“场景之间无隐藏副作用依赖”在代码层面可验证。浏览器与 API 身份分离startSession中浏览器上下文仅在authenticated时注入storageState而consoleRequestContext始终持有独立的 API 请求上下文。e2e/AGENTS.md 对此的解释是让浏览器身份与 API 身份保持分离未认证unauthenticated和登出logout旅程才不会把 fixture 的 ownership 搞乱。清理是 World 的一等公民registerCleanup(...)收集场景内注册的清理回调runRegisteredCleanups以 LIFO 顺序执行并与类型化清理队列配合。这支撑了“场景拥有自己创建的临时资源并必须注册清理”的契约。类型化的this: DifyWorld写法Cucumber.js 支持通过world代理在箭头函数中访问 World但 Dify 套件一致性地使用类型化的async function (this: DifyWorld, ...)函数式写法SKILL.md 与 e2e/AGENTS.md 均将其列为硬性约定。原因是箭头函数无法接收 Cucumber 绑定的 World 实例而显式的this注解让 World 的所有权和 TypeScript 类型发现都保持明确。仓库中所有步骤定义如 create-app.steps.ts都严格遵循这一模式。一个“薄步骤”的完整示例create-app.steps.ts 中的I confirm app creationWhen(I confirm app creation, async function (this: DifyWorld) { const page this.getPage() const createButton page.getByRole(dialog).getByRole(button, { name: /^Create(?:\s|$)/ }) const responsePromise page.waitForResponse(/* 匹配 POST /console/api/apps */) await expect(createButton).toBeEnabled() await createButton.click() const response await responsePromise expect(response.ok()).toBe(true) const createdApp zPostAppsResponse.parse(await response.json()) this.createdAppIds.push(createdApp.id) })Gherkin 里只有一个连贯动作“我确认创建应用”而按钮定位、响应等待、契约校验zPostAppsResponse是生成的 oRPC 客户端的 zod schema、状态记录这些实现细节全部被封装在步骤内部——规格保持声明式自动化保持完整。原则六有意地使用标签原文档对标签的界定是标签可以传达稳定的能力分组、选择范围、fixture 依赖或条件钩子意图只有当 runner、seed profile 或钩子真正“拥有”这个含义时标签才会改变运行时行为。换句话说一个不被任何运行逻辑解释的标签就是纯粹的自欺。Dify 的标签体系定义于 e2e/AGENTS.md 的 “Tags And External Runtime” 一节是这条原则的完整实例标签运行时归属unauthenticated创建干净无存储状态的浏览器上下文authenticated仅是意图与选择标签不改变运行行为smoke用于选择冒烟子集如pnpm -C e2e e2e -- --tags smokeaxe/wcag-a/wcag-aa/wcag-page-slugWCAG 独立扫描体系被默认功能套件排除选级别时必须同时选axeprepared依赖 seed 的 prepared fixturepost-merge seed profile 会提供external-model/external-tool依赖真实外部运行时确定性命令默认排除外部命令才可选microphone使用检入的假音频 fixture 与隔离的 Chromium 上下文browser-smoke在 Chromium 与 WebKit CI 通道运行聚焦的键盘与导航覆盖skip从所有 runner profile 中临时排除产品行为恢复后必须移除禁止用于永久或环境相关的抑制agent-backend-runtimeAgent v2 运行时场景需要显式的运行时可用性步骤与E2E_START_AGENT_BACKEND1或后端 URL标签在 runner 层如何“被拥有”可以在 cucumber.config.ts 中直接看到const defaultNonExternalTags not axe and not prepared and not external-model and not external-tool const selectedTags process.env.E2E_CUCUMBER_TAGS || (hasCliTags ? undefined : defaultNonExternalTags) const tags selectedTags ? (${selectedTags}) and not skip : not skip const config { format: [progress-bar, summary, html:./cucumber-report/report.html, message:./cucumber-report/report.ndjson], import: [./tsx-register.js, features/**/*.ts], paths: [features/**/*.feature], tags, timeout: 60_000, }这段配置说明了两件事默认命令会自动排除 WCAG 扫描、prepared fixture 场景与外部运行时场景且无条件排除skip而smoke、authenticated这类标签只有在选择器层面生效。原文档最后一问——“新标签是在记录真实行为还是在发明套件并未实现的语义”——在这套配置下有非常具体的检验标准如果 runner、seed 或钩子都不解释它就不要加它。评审清单合并前的九个问题原文档给出的 Review Questions 是评审 Gherkin 与步骤定义时的完整检查表建议逐条过这个场景读起来像产品行为的真实示例吗它证明的是一个结果还是把几个独立阶段揉在了一起步骤是面向行为的还是面向实现的过程性措辞对“被测行为”是否必要一个被复用的步骤在这个 feature 里仍然保持诚实吗新表达式是否会在全局命名空间中与已有步骤重叠用Rule能澄清业务规则还是Background会隐藏重要的 setup新标签是在记录真实行为还是在发明套件并未实现的语义一个新读者在不打开步骤定义文件的情况下能理解这个场景的结果吗落地如何在 Dify 仓库中运行与验证规范最终要落到可执行的命令上。根据 e2e/AGENTS.md从仓库根目录执行依赖与浏览器只需安装一次pnpm install与pnpm -C e2e e2e:install同一时间只运行一个本地e2e*进程因为 runner 共享端口、认证状态与日志路径# 对已有初始化好的实例直接运行 pnpm -C e2e e2e # 重置、初始化并运行确定性场景 pnpm -C e2e e2e:full # 只跑冒烟子集标签选择 pnpm -C e2e e2e -- --tags smoke # 有头模式调试可配合 E2E_SLOW_MO500 放慢操作 pnpm -C e2e e2e:headed -- --tags smoke # 依赖 prepared fixture 的场景 E2E_START_AGENT_BACKEND1 pnpm -C e2e e2e:prepared结果产物失败会在cucumber-report/artifacts/生成截图与 HTML 捕获HTML 报告与 Cucumber Messages 报告位于cucumber-report/下前后端启动日志在.logs/。静态检查可用vp check e2e。写改动时的纪律与 SKILL.md 的工作流一致先复用措辞与行为都匹配的既有步骤都不匹配时再加一个连贯的场景或步骤改动后跑最窄的标签化场景只有当改动触及共享钩子、标签或 support 代码时才扩大运行范围。小结这份规范的核心思想可以浓缩为一句话让 Gherkin 停留在产品行为层把机制、状态与清理压进它们各自的 owner。在 Dify 的 e2e 套件中这体现为按能力域组织的步骤定义与全局唯一命名空间、类型化this: DifyWorld的薄步骤、每场景重建的 World 状态与 LIFO 清理注册表、以及每个标签都有 runner/seed/钩子解释的标签体系。对维护 E2E 套件的工程师来说这套实践的价值不在于让测试跑起来而在于让测试在半年后依然读得懂、找得到、信得过。【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表