ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cucumber BDD测试框架实战指南:从Gherkin语法到自动化测试落地

Cucumber BDD测试框架实战指南:从Gherkin语法到自动化测试落地 1. 先从为什么用它说起BDD和Cucumber的价值边界刚接触Cucumber的人十有八九会有一种错觉这不就是一个能用自然语言写测试的工具吗把测试步骤翻译成英文句子再用代码去匹配本质上跟JUnit、TestNG没区别。这个理解对了一半——Cucumber确实是这么工作的但如果你只把它当成一个能读英文的测试框架那它真正的价值你一点都没用上。我最早接触Cucumber是在一个遗留系统改造项目里。当时核心业务模块的回归测试散落在各个测试类里每个开发写自己的风格断言方式五花八门。业务方提需求时说的是用户逾期3天未还款系统自动发送提醒短信到了测试代码里就变成了一段没人看得懂的Java逻辑。需求和测试之间隔着一道巨大的鸿沟业务人员看不懂测试开发也没法快速确认这条测试到底覆盖了哪条业务规则。引入Cucumber之后最大的变化不是自动化率提升了多少而是产品经理终于能指着feature文件说这条场景写错了我们实际的业务规则是逾期后先发邮件再发短信不是只发短信。这正是Cucumber的核心价值它不是替代你的测试框架而是架设在业务需求和自动化测试之间的一座翻译桥。它用Gherkin这种接近自然语言的语法把测试用例写成业务人员也能读懂的可执行规格说明Executable Specification。那它适合谁用我个人的判断是团队里业务方愿意参与需求验收的或者至少需要测试用例能被非技术人员审阅的业务规则复杂、分支多测试用例经常随需求变更而需要同步维护的跨团队协作时需要一套统一语言来描述系统行为的反过来如果你的团队里业务方完全不看测试所有用例都是开发自己写给自己看那Cucumber带来的额外成本可能会大于收益。这一点后面细说先给你打个预防针。1.1 任何一个测试框架都能写断言Cucumber的差异在哪里先说句得罪人的话用Cucumber写断言本身并没有比JUnit或pytest多出什么魔法能力。它不提供更好的Mock库不内置更强大的断言语法也不解决测试数据构造问题。它的差异化能力集中在两个层面。第一是可读性即文档。一个写好的feature文件本身就是一份活的规格说明书。它不像传统测试那样只描述输入什么、断言什么而是描述作为什么角色、在什么背景下、执行什么动作、期望什么结果。这套结构不是Cucumber发明的它来自BDDBehavior Driven Development的Given-When-Then范式Cucumber只是把这一范式落地成了工具。当你用Cucumber写了一年测试之后回头看那些feature文件的价值已经不亚于架构文档了——因为它们描述的是系统对外的行为契约。第二是驱动开发流程。BDD的完整流程是先写feature文件再写step definitions最后写实现代码。也就是说测试用例是前置的它在你写业务代码之前就定义了系统应该有的行为。这跟传统的先写代码、后补测试完全是两条路径。很多团队在实际落地时会把流程简化成需求评审时产出feature文件开发实现完成后补上step definitions这也是一种可行模式但核心不变feature文件是先行的。1.2 明确边界Cucumber不是用来替代单元测试的这是我在实战中最想强调的一点。很多团队在初期引入Cucumber时会犯同一个错误试图把所有测试都写成Cucumber场景恨不得把一个工具类方法都包一层Given-When-Then。结果就是feature文件爆炸step definitions越来越臃肿执行时间越来越长最后整个测试套件变成了谁也动不了的巨型怪物。Cucumber擅长的是端到端场景、业务规则验证、跨模块交互这类行为级测试。而单元测试、方法内部算法校验、异常分支的细粒度验证这些应该留给JUnit、TestNG或者你惯用的单测框架。正确的分工是单元测试保证代码逻辑的正确性Cucumber保证业务行为符合预期。前者是代码没写错后者是做的是对的。我在项目里一般会用这条标准来判断一个测试是否应该写成Cucumber场景这个用例的描述能否用一句话让非技术背景的业务人员看懂如果答案是不能那它大概率适合做普通单元测试而不是Cucumber场景。这个判断标准帮我避免过很多次过度设计。2. 环境搭建与项目骨架比想象中容易踩坑的第一步Cucumber支持的语言生态很多Java、JavaScript、Python、Ruby、Go都能跑。我自己的主力语言是Java下面的示例也以Java生态为主但核心思路放到其他语言上完全通用。2.1 语言生态选择与依赖安装如果你用的是Maven在pom.xml里加这几项就够了dependency groupIdio.cucumber/groupId artifactIdcucumber-java/artifactId version7.14.0/version scopetest/scope /dependency dependency groupIdio.cucumber/groupId artifactIdcucumber-junit-platform-engine/artifactId version7.14.0/version scopetest/scope /dependency这里有个容易踩的坑cucumber-junit-platform-engine 需要配合 JUnit 5 使用它会通过 JUnit Platform 发现并执行场景。如果你用的是 JUnit 4那应该依赖 cucumber-junit 而不是这个。在项目升级时如果搞混了最典型的现象是运行测试时什么都不跑控制台安静得像什么都没发生但测试结果是绿色的。另外一个常见问题是版本不一致。Cucumber的依赖之间要求版本统一如果你在pom里写了cucumber-java 7.14.0而cucumber-junit-platform-engine写的却是7.10.1运行时大概率会报奇怪的兼容性错误。这类问题排查起来很费时间所以我的建议是Cucumber的所有子模块用同一个版本升级时一起升。2.2 最小可运行项目从目录结构到第一个feature一个标准的Cucumber项目结构是这样的src/ ├── test/ │ ├── java/ │ │ └── com/example/ │ │ ├── stepdefs/ │ │ │ └── LoginStepDefinitions.java │ │ └── RunCucumberTest.java │ └── resources/ │ └── features/ │ └── login.featureRunCucumberTest是入口类它负责启动整个测试套件。最简单的方式是用JUnit Platform的Selectorpackage com.example; import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; Suite IncludeEngines(cucumber) SelectClasspathResource(features) ConfigurationParameter(key GLUE_PROPERTY_NAME, value com.example.stepdefs) public class RunCucumberTest { }这里有两个关键点。SelectClasspathResource(features)指向src/test/resources/features目录Cucumber会从该路径读取所有.feature文件。GLUE_PROPERTY_NAME指定了step definitions的包路径Cucumber从这个包里扫描所有标注了Given、When、Then注解的方法。第一个feature文件怎么写我给你一个最经典的登录场景Feature: 用户登录 作为注册用户 我希望使用账号密码登录系统 以便访问我的个人中心 Scenario: 使用正确的账号密码登录成功 Given 用户已经注册账号为testuser密码为123456 When 用户在登录页面输入账号testuser和密码123456 And 用户点击登录按钮 Then 系统跳转到个人中心页面 And 页面显示用户名testuser注意Feature关键字下面的三行描述它们不是可执行的步骤而是这个功能模块的背景说明用作为/我希望/以便这种句式来表达业务动机。虽然Cucumber执行时会忽略它们但从文档化的角度来说这三行是整个feature文件里最有信息量的部分——它告诉你这个功能是给谁用、解决什么问题的。2.3 环境配置里常见的三个坑第一个坑是feature文件编码问题。中文场景下如果文件不是UTF-8编码Gherkin解析器很可能把中文步骤识别为乱码导致undefined step报错。我遇到过一次很诡异的情况同事提交的feature文件是GBK编码本地跑得好好的CI上一跑就报无法解析步骤。排查了半天才发现是编码不一致。解决办法是全局统一项目编码为UTF-8properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding project.reporting.outputEncodingUTF-8/project.reporting.outputEncoding /properties第二个坑是step definitions扫描不到。明明写了带Given注解的方法运行时却提示undefined step。这多半是GLUE路径配错了或者step definitions所在的包不在glue配置的路径下。这里要特别提醒glue路径是包路径而不是类路径不要写成com.example.stepdefs.LoginStepDefinitions这种具体类路径。第三个坑是重复的step definitions。同一个匹配规则在两个类里各写了一次Cucumber启动时会直接报错Ambiguous步进定义。这个在团队协作时特别容易触发后面在讲步骤管理时我会细说如何规避。3. Gherkin语法精讲写好功能文件是门手艺很多人在Cucumber上翻车不是技术能力不够而是没把Gherkin当成一种正经语言来学。有人觉得Gherkin不就是英文句子嘛随便写写就行。结果就是feature文件写得像流水账step definitions匹配规则写得千奇百怪。实际上Gherkin有一套严格的语法约束理解了这套约束你才能从能跑升级到写得好。3.1 六大关键字与语义约束Gherkin的核心关键字有Feature、Rule、Example/Scenario、Given、When、Then、And/But、Background、Scenario Outline、Scenario Template等。说六大关键字有点不完全但对日常使用来说你先把这组规则吃透就够了Feature功能模块的总描述给出了整个文件的业务范围最好控制在几句话内。Scenario一条可执行的行为场景。一个feature文件下可以有多个Scenario每个Scenario互相独立。Given上下文前置条件。它描述系统处于什么状态不是用户执行的动作。When用户触发的动作。这是场景中唯一的事件。Then动作产生的结果是断言所在的位置。And/But连接连续的Given/When/Then让步骤列表更像自然语言的并列递进。有一个很经典的原则Given、When、Then在一个场景里应该按状态→事件→断言的顺序出现不推荐交叉重复。比如Given ... And ... When ... And ... Then ... And ...这种顺序就是标准的。我在评审团队代码时经常看到一个问题有人在Then后面继续写When把第二个操作放在断言之后。这样语义会变得混乱。比如Scenario: 错误处理 Given 用户已登录 When 用户提交无效订单 Then 系统返回错误提示 When 用户重新提交有效订单 Then 系统成功创建订单这种写法不是不行但它更像是两条场景被强行合并成了一条。正确做法是拆成两个独立的Scenario分别验证无效订单被拒绝和有效订单被接受两种行为。拆开后会更容易定位失败点。3.2 用数据表和Scenario Outline消灭重复场景假设你要验证不同手机号段注册时验证规则不同没有数据驱动的时候你只能复制粘贴多条Scenario内容几乎一样只是参数不同。这种重复既难看又难维护。Scenario Outline就是专门解决这个问题的Scenario Outline: 手机号段注册规则 Given 系统已开启手机号注册 When 用户输入手机号phone And 点击获取验证码 Then 系统提示expected Examples: | phone | expected | | 13812345678 | 验证码已发送 | | 17012345678 | 当前号段暂不支持 | | 123 | 手机号格式不正确 |phone和expected是占位符实际执行时Cucumber会用Examples表格里的每一行数据替换占位符一行就是一条独立的场景。第二条数据跑挂了不会影响第一条这是一项非常实用的能力。下面说说DataTable它是另一种场景数据组织方式。不同的是Scenario Outline用于参数化同一场景DataTable是在单个步骤中传入结构化数据集。这两者的语法完全不同不要搞混Scenario: 批量导入用户 Given 管理员已登录 When 管理员导入以下用户数据 | 用户名 | 邮箱 | 角色 | | zhangsan | zhangsanxx.com | 编辑 | | lisi | lisixx.com | 管理员| Then 系统成功创建2个用户DataTable的具体读取方式到第5部分实操时详说。3.3 写feature时最容易犯的严重错误先记住一个原则feature文件是规格文档不是操作脚本。我发现不少初学者的feature写得像UI测试脚本Scenario: 用户登录 Given 打开浏览器 When 输入用户名admin And 输入密码123456 And 点击按钮登录 Then 等待3秒 And 断言页面显示欢迎回来这条场景最大的问题在于它的每一个步骤都绑定在UI细节上。将来页面改版输入框从input变成datalist登录按钮换了个位置这条场景就得重写。更糟糕的是等待3秒这种硬编码等待在Cucumber从业者眼里就是明确的坏味道。正确的思路是关注业务行为去掉UI实现细节让步骤描述意图而非操作。同样是登录应该写成用户使用正确的账号密码登录成功至于他用的是Web页面、移动端还是API那是step definitions里要做的事不是feature文件里要管的事。这样一旦底层UI变了只需要改step definitions的那一个方法feature文件可以完全不用动。另外一定要控制feature文件的行数。一条Scenario超过10个步骤可读性会迅速下降。如果出现了Given...And...And...When...And...And...Then...And...And这种长链条请先反思是不是把太多不相干的行为塞进了一条场景通常这时候拆成多条Scenario会更清晰。4. Step Definitions绑定机制让英语句子变成可执行代码Cucumber的执行核心就是将Gherkin步骤绑定到Java方法这一步依赖的是匹配规则。虽然概念简单但实际写起来细节很丰富。4.1 匹配规则与正则表达式的正确用法一个step definition本质是被注解修饰的方法。比如package com.example.stepdefs; import io.cucumber.java.en.Given; import io.cucumber.java.en.When; import io.cucumber.java.en.Then; public class LoginStepDefinitions { Given(用户已经注册账号为{string}密码为{string}) public void userRegistered(String username, String password) { // 准备测试数据比如插入数据库或调用mock接口 } When(用户在登录页面输入账号{string}和密码{string}) public void userInputCredentials(String username, String password) { // 执行登录操作 } Then(系统跳转到个人中心页面) public void shouldNavigateToDashboard() { // 断言跳转成功 } }{string}是Cucumber表达式中的参数占位符它会匹配带引号的字符串并自动作为参数传入方法。这是Cucumber 7.x默认使用的表达式语法。Cucumber表达式与正则的区别要分清。{string}匹配的是引号包裹的文本比如testuser它只会拿testuser做参数。如果你想匹配不带引号的自由文本得用{word}或者自己写正则。比如Given(手机号段为{word}的用户) public void userWithPhoneSegment(String segment) { // segment 就是 170 }当内置的Cucumber表达式满足不了复杂需求时可以退回到正则模式写法是把注解里的内容用^和$包起来并用正则语法编写Given(^用户已经注册账号为\([^\]*)\密码为\([^\]*)\$) public void userRegistered(String username, String password) { // 这里的正则匹配逻辑等价于 {string} }两种模式各有使用场景。大部分情况下我推荐用Cucumber表达式因为可读性更好、参数类型也会自动转换。只有当你需要做或匹配比如登录/登出合并在一个方法里时才用正则。4.2 undefined step背后的排查思路这是Cucumber新手碰到的头号问题高居Stack Overflow提问榜首。它的本质很简单Cucumber在glue路径下扫描了所有step definitions方法没有找到能和Gherkin句子匹配上的规则。排查时我一般按这个链路走看报错提示中的具体步骤检查中文字符是否一致。最典型的坑是feature文件里写的是用户点击【登录】按钮step definitions里写的是用户点击[登录]按钮一个全角中括号一个半角肉眼很难发现但匹配就是失败。检查参数类型是否匹配。比如step definitions用{int}但你在feature里写的是字符串testuser那同样匹配不到。如果用的是正则模式检查正则表达式是否精确匹配整句话。正则模式默认要求全串匹配你的正则是^.*登录而实际句子还有后续内容就会匹配失败。最后确认glue路径配置完整特别是你新增了stepdefs子包但忘了把它纳入glue扫描范围时Cucumber只会报undefined step不会提示你有个类没扫到。实践中绝大多数undefined step问题出在第一步和第四步。建议用IDE里的Cucumber插件比如IntelliJ的Cucumber for Java插件它能在feature文件和step definitions之间做跳转没匹配上的步骤会直接标红能省掉一大半低级错误。4.3 步骤定义的组织与管理当项目里有几百条step definitions之后你会发现管理它们比写它们更困难。我在项目里见过两种典型的反面教材第一种是所有方法都塞进一个StepDefinitions类——最后那个类超过一千行任何改动都可能引发其他场景的连锁问题。第二种是一个场景一个类——每个类里只有两三个方法结果产生了几十个类互相引用的状态全靠静态变量传递。正确的组织方式是按业务域划分而不是按技术层划分。比如用户模块、订单模块、支付模块各建一个StepDefinitions类。类内部可以按数据准备、操作动作、结果断言再分段组织方法。当场景涉及跨模块交互时按业务主流程归属到主模块的类里。另外我强烈建议在团队里设立step definitions复用规范。两个方法匹配同一个Gherkin句子时Cucumber会直接抛ambiguous错误。要避免这种问题最好的办法不是在出错后去改注解而是维护一个共享的步骤词汇表文档。团队约定好常用动作的表达方式比如用户已登录统一用这句而不是已登录用户或用户登录状态为已登录从源头上避免重复定义。5. 数据表、文档字符串与上下文共享处理复杂输入输出Feature文件里除了简单的{string}参数还有几种更复杂的传参方式一旦用对了表达力会强很多。5.1 DataTable的三种用法看上面的批量导入用户示例。在step definitions里读取DataTable的标准姿势如下import io.cucumber.datatable.DataTable; import io.cucumber.java.en.When; import java.util.List; import java.util.Map; When(管理员导入以下用户数据) public void importUsers(DataTable dataTable) { ListMapString, String rows dataTable.asMaps(String.class, String.class); for (MapString, String row : rows) { String username row.get(用户名); String email row.get(邮箱); String role row.get(角色); // 逐行处理 } }DataTable还有另外两种常见形态dataTable.asList(YourType.class)把表格直接转成List 前提是表头名和对象属性名对应得上。dataTable.raw()返回ListListString适合表头不固定、你需要自己处理单元格数据的场景。选择哪种读取方式取决于该步骤的结构化程度。表头固定时用asMaps要批量构造对象时用asList完全自由格式时用raw()。我见过有人不分场景一律用raw()然后自己遍历代码写起来麻烦而且容易出错其实用asMaps更干净。5.2 DocString传参较长的文本输入比如请求体JSON、一封邮件的正文内容放进DataTable里会很难维护这时可以用DocStringScenario: 创建文章 Given 作者已登录 When 作者提交如下文章内容 {title: Cucumber实战, content: 这是一篇关于BDD测试框架的文章, tags: [BDD, Cucumber]} Then 系统返回创建成功在step definitions里对应When(作者提交如下文章内容) public void submitArticle(String docString) { // docString 就是上面三引号里的JSON字符串 // 可以用 Jackson 或 Gson 解析成对象 }DocString非常适合场景中包含长文本的场景。但要注意它默认不做任何转义JSON里如果含有双引号直接照抄进Gherkin会出问题。实际项目中我一般把这种长文本拆到测试资源文件里然后step里只传文件路径避免直接把长JSON堆进feature文件尤其当内容很长时那会严重影响可读性。5.3 World/Context对象的设计在Cucumber-Java里多个step definitions方法之间共享状态有几种手段。常见做法是使用一个Scenario级别的Context对象public class TestContext { private String currentUser; private HttpResponse response; private MapString, Object data new HashMap(); // getter/setter }配合PicoContainer或Spring来管理依赖注入把TestContext注入到各个StepDefinitions类中。这样同一个Scenario内的不同step之间可以共享数据Given方法先执行登录When方法读取Context里的登录TokenThen方法再用Token去断言其他结果。这里要特别强调生命周期问题Cucumber默认每个Scenario会创建一个新的World/Context实例这样场景之间天然隔离互不污染。如果你用了Spring或者手动写静态变量来共享状态就必须小心场景残留问题——上一个场景遗留的静态状态会污染下一个场景的执行结果。我在项目里是这么约定的普通场景内共享的数据放TestContext非静态跨场景共享的只读配置放环境配置文件场景之间不允许有依赖关系也不允许依赖执行顺序这一条规矩非常管用。没有这条约定之前团队经常出现A场景单独跑是绿的全量跑就挂的诡异问题查下来基本都是静态变量污染。6. Hooks、标签与执行策略工程化的分水岭当你从跑通Demo进阶到在真实项目里稳定运行就必须开始关注场景的组织与执行策略。Cucumber在这方面的能力集中在Hooks和标签上。6.1 Hook的执行顺序Hook是Cucumber生命周期的钩子能在特定时间点执行一些准备或清理工作。最常见的是Before和Afterimport io.cucumber.java.Before; import io.cucumber.java.After; public class Hooks { Before public void setUp() { // 初始化浏览器驱动 / 清理数据库 / 生成测试数据 } After public void tearDown() { // 关闭浏览器 / 清理测试环境 / 保存失败截图 } }Before在每条Scenario执行前触发After在每条Scenario执行后触发无论成功失败。需要更精细的控制时可以用Before(tag)的形式只对带特定标签的场景生效。执行顺序方面Cucumber保证同一类别内的多个Hook按不确定顺序执行但你可以用Before(order 10)来排序数字越小越先执行。After的排序规则相反数字越小越后执行。项目中如果需要先开浏览器再登录的先后关系就靠order来控制顺序。我习惯定一套约定全局环境初始化order100数据准备order200业务相关操作order300这样新增Hook时只需要按数值范围安排即可。6.2 标签管理与场景过滤标签是feature文件里以开头的内容可以写在Feature行或Scenario行。比如smoke login Feature: 用户登录 Scenario: 正确账号密码登录成功 ... regression Scenario: 账号被锁定后无法登录 ...标签的价值不只是给场景分分组更关键的是可以配置只跑哪些测试。在Runner类上加上ConfigurationParameter(key FILTER_TAGS_PROPERTY_NAME, value smoke and not ignore)这行的意思是执行所有带smoke且不带ignore的场景。利用这个机制你可以轻松实现每天跑冒烟测试、每次提交跑回归测试、特定模块单独跑的灵活策略。标签的管理建议遵循一套统一的命名规范。项目里常见的标签维度有运行层级smoke、regression、e2e模块归属user、order、payment状态标记ignore、wip设备平台web、mobile、api不要设计太多标签标签越多组合维护的复杂度越高。一个场景合理打上两三个标签就够了。6.3 重试机制与失败场景隔离UI自动化最让人头疼的就是偶发性失败——这次跑挂了下次跑又好了完全复现不出来。Cucumber本身没有内置重试机制但可以通过Runner配置加上重试能力。在JUnit Platform版本里可以这样配置ConfigurationParameter(key EXECUTION_DRY_RUN_PROPERTY_NAME, value false)关于重试官方实现有额外扩展也可以借助第三方库或CI层的重跑策略来实现。我的态度是重试是缓冲区不是万能药。如果一个场景的失败率超过10%正确的做法不是加大重试次数而是定位根本原因——多半是等待条件不对、测试数据冲突或环境不稳定。把重试当成最后手段而不是第一选择。另外要养成一个习惯场景代码要尽量幂等。一个场景无论重复执行多少遍结果都应该是确定的。常见的反面例子是场景里写死后读数据库获取当前时间进而影响断言结果这样的场景天然不稳定。保证幂等的主要手段是所有测试数据在开始前清理、结束后恢复或者使用Mock数据源。7. 真实项目中的集成套路与踩坑实录把前面这些概念放进真实项目里你就会遇到怎么跟Selenium配合怎么跟API测试组合怎么把报告接进CI这类问题。这一节我挑最典型的几个场景来讲。7.1 与UI自动化工具集成Cucumber配合Selenium或Playwright是最常见的组合。Selenium执行Web页面操作Cucumber提供场景组织和自然语言描述两者各司其职。关键点在于不要让Cucumber的step definitions里出现大量Selenium代码。我的做法是封装一层Page Objectstep definitions只做把Gherkin步骤翻译成对Page Object的调用。例如When(用户在登录页面输入账号{string}和密码{string}) public void inputCredentials(String username, String password) { loginPage.inputUsername(username); loginPage.inputPassword(password); }这样做的原因是隔离变化。网页的DOM结构随时会变如果step definitions里直接写driver.findElement(By.id(username))一旦页面改版step definitions里要改的地方成倍增加。用Page Object封装一层页面变化时只需要改Page Object类。跟Playwright集成时思路一样。Playwright对等待策略的处理比Selenium好很多默认有auto-wait测试更稳定。但要注意Cucumber场景中的等待依然不该用Thread.sleep而是用显式等待或依赖Playwright的auto-wait机制。7.2 与API测试集成很多团队在做服务端接口回归测试时也会用Cucumber。这种场景下feature文件写的是接口行为step definitions里发HTTP请求、断言JSON响应。跟UI测试相比API测试要稳定得多CI执行速度也快很多非常适合做持续回归。一个完整的API测试场景长这样Scenario: 创建订单成功 Given 用户user001已获取访问令牌 When 用户创建订单商品ID为P001数量为2 Then 系统返回订单号 And 订单状态为待支付step definitions里用RestAssured或类似工具发请求When(用户创建订单商品ID为{string}数量为{int}) public void createOrder(String productId, int quantity) { String token context.getToken(); context.setResponse( RestAssured.given() .header(Authorization, Bearer token) .body(Map.of(productId, productId, quantity, quantity)) .post(/api/orders) ); } Then(系统返回订单号) public void shouldReturnOrderId() { context.getResponse().then().statusCode(200); String orderId context.getResponse().jsonPath().getString(orderId); Assertions.assertNotNull(orderId); context.setOrderId(orderId); }这一步要特别注意API测试的feature文件不要跟UI测试混用场景。一个是接口行为一个是页面行为混在一起后feature文件的层级和业务含义会变得非常混乱。我建议按目录区分比如features/api目录和features/ui目录分别对应不同的Runner配置。7.3 CI流水线、报告生成与失败回溯没有CI的Cucumber测试是不完整的。在CI流水线里我一般会配置两个任务一个是提交时运行的快速任务只跑smoke标签一个是每晚运行的完整任务跑regression。用标签过滤控制任务范围避免每次提交都跑几小时的全量测试。报告方面Cucumber支持生成HTML和JSON格式的报告。经典的插件有cucumber-reporting和自带的pretty、json插件。推荐的配置是在Runner上让Cucumber生成JSON报告再由CI插件或Maven插件将其转换为美观的HTML页面plugin groupIdnet.masterthought/groupId artifactIdmaven-cucumber-reporting/artifactId version5.7.5/version /plugin报告里最重要的信息是失败场景的详细信息、失败步骤和异常堆栈。如果做了截图也建议在Then断言失败时自动保存截图并附加到报告中After public void takeScreenshotOnFailure(Scenario scenario) { if (scenario.isFailed()) { byte[] screenshot ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); scenario.attach(screenshot, image/png, failure-screenshot); } }7.4 我踩过的三个最典型的坑坑一场景间共享数据库状态。团队早期没有人管理测试数据的清理导致场景A创建了一条记录场景B统计数量时把A的记录也算进去了结果时好时坏。后来统一在Before里做数据准备After里做数据清理并为测试数据加上了固定前缀标识问题被彻底根治。坑二Cucumber与Spring集成时的事务回滚失效。在Spring Boot项目里如果应用层开启了事务管理但你的Cucumber测试没有让整个测试运行在同一事务上下文中那么测试结束后的回滚不会生效。我见过有人花了两个星期排查为什么我每次跑测试数据库都在涨数据最终发现Cucumber的Hook和Spring事务根本没有绑定在同一个线程。解决方式是使用专门的测试配置类或清理方案来管理数据。坑三并行执行导致的资源冲突。Cucumber本身支持并行执行场景但前提是场景之间完全独立。如果两个场景同时操作同一个测试账号或者同时往同一个CSV文件里写数据就会出现偶发性失败。我建议并行执行前先做隔离性审查确保每个场景使用独立的数据或者方案是通过线程私有变量保证数据不冲突。8. 一些实战后的心得与建议最后聊几件测试框架之外的事。第一件事Cucumber最难的从来不是技术而是让所有人用同一种语言说话。如果业务方不参与团队内部对同一个业务动作的表述又千差万别那feature文件很快就会变成一盘散沙。我的建议是找一个长期存在的核心业务模块拉上业务分析师和测试负责人花半天时间整理一份场景步骤词汇表从此以后所有feature文件都使用这套词汇表。不要小看这件事它的价值会在三个月后某个测试挂掉时充分体现出来——你一眼就能看懂这个场景在测什么。第二件事任何测试框架都会在生产力和维护性之间找平衡。Cucumber的低代码成本带来的是表达能力但它不适合做细粒度断言测试。如果你发现项目里大多数step definitions都只有一两行代码、只是把普通测试方法包了一层英文皮那就要反思引入Cucumber是否真的带来了价值。我见过一些团队为了用Cucumber而用Cucumber最后维护成本反而超过收益。这不是Cucumber的问题是使用场景没选对。第三件事Cucumber社区已经迭代了很多版本Cucumber 7.x之后的特性变化不小。如果你是在老项目上做升级请先看官方迁移文档特别注意StepDefinitions的包名变化、cucumber-junit与cucumber-junit-platform-engine的关系以及新表达式语法的兼容性。我在上一个大版本升级时就因为有老代码还在用旧的正则匹配模式整整花了半天调兼容性。如果你恰好也在评估或使用Cucumber希望这篇参考对你有点帮助。以上这些坑和建议都是我一个个踩出来和试出来的经验虽然没有放之四海皆准的银弹但按这套思路走至少能让你的Cucumber测试少一点玄学失败多一点可解释、可追溯、可维护。
返回列表