用Cucumber让自动化测试成为团队通用语言:从BDD到工程落地

发布时间:2026/10/13 1:43:13
用Cucumber让自动化测试成为团队通用语言:从BDD到工程落地 你们有没有经历过这种尴尬自动化测试代码写完了用例也跑起来了结果一到评审会业务方盯着满屏的assertEquals和元素定位表达式问了一句“这到底验证的是什么业务规则”整个会议室瞬间安静。这不是我编的而是很多团队做自动化测试时的真实场景。后来我在项目里引入Cucumber这个自动化测试框架问题才算真正解决。它不是让测试更会写代码而是让整个团队——包括业务、产品、开发、测试——都能用同一种语言描述“系统应该做什么”。这篇文章照我的实际使用经验来写适合正在做功能自动化、接口自动化想引入 BDD 或者想优化当前测试框架的团队参考。1. 别急着写代码Cucumber 解决的是“三拨人各说各话”1.1 Cucumber 的定位一个用自然语言驱动测试的框架先泼一盆冷水不少人第一次用 Cucumber 就犯迷糊——装了依赖、写了 Feature 文件却发现 Cucumber 自己连浏览器都打不开。这里必须把定位说清楚Cucumber 本身不驱动浏览器也不发 HTTP 请求。它只负责三件事解析用 Gherkin 写的 Feature 文件、根据关键字匹配对应的 Step Definitions、把匹配到的代码按场景顺序执行。真正做页面操作的是 Selenium WebDriver真正发接口请求的是 RestAssured、HttpClient 这类库。形象一点理解把 Cucumber 比作“可以执行的 PRD”。产品经理写的需求文档如果足够结构化理论上可以被机器直接执行——Given/When/Then 就是这种结构化的最小单元。所以当你决定引入 Cucumber先别急着写代码第一步是检查团队里有没有人能把这些规则用自然语言写清楚。写不清楚用什么框架都白搭。1.2 Given / When / Then 为什么这么设计自动化测试界写了很多年的“三步曲”准备数据、执行业务、校验结果。换成 Gherkin就是Given、When、Then额外加两个辅助角色And和But。这个拆分不是为了让语法好看而是对应人类理解业务的方式。Feature: 用户登录 Scenario: 使用正确凭证能够登录成功 Given 用户访问登录页面 When 用户输入正确的用户名和密码 When 用户点击登录按钮 Then 用户应该看到首页按单元测试的 AAA 理论套Given是 Arrange前置条件When是 Act用户行为Then是 Assert结果断言。这样拆开之后最大的收益有两个第一同一段Given可以复用在多个Scenario里第二任何一个人打开 Feature 文件不看代码就知道这条用例在验证什么业务。这一点对国产软件团队尤其受用——需求文档更新不及时几乎是常态Feature 文件到最后往往比文档还新、还准。1.3 谁写 Feature理想与现实的折中方案很多人对 BDD 的想象是业务人员、开发、测试围一桌大家一起写 Gherkin。实话说我在项目里没见过几个业务总监愿意亲手写Scenario的。现实做法通常是测试牵头草拟产品和开发评审确认。我在过的团队是这样运作的测试拿到新需求后用 Gherkin 写一版验收场景当作“需求理解确认单”发给产品和开发。评审会上大家不聊代码只聊规则“没登录的用户点结算是不是应该弹登录框”“订单状态为已取消还能不能申请退款”这些对话如果能在写自动化代码之前发生返工成本可以压到很低。Feature 文件一旦定稿它既是需求验收标准又是自动化用例的源码一份投入两份产出。2. 技术栈盘点为什么 Java 项目里我会首选 Cucumber2.1 Java、Python 两套生态到底怎么选搜自动化测试框架相关的内容绕不开 Java 和 Python 两个阵营。Python 生态里大家更熟悉 pytest对应 Cucumber 习惯的是pytest-bdd、behaveJava 生态则基本是 Cucumber-JVM 一家独大配合 JUnit/TestNG 使用。这里给个不含糊的选型建议项目技术栈是 Java 系接口自动化、Web 自动化都有 Java 代码积累直接上 Cucumber团队主体是 Python 写测试脚本就没必要强行转 Javapytest-bdd 在 pytest 的 fixture 体系下融合得更好。选型不是比谁功能多是比谁融进现有工程时阻力小。表格对比一下两个生态的特征对比项Java CucumberPython pytest-bdd / behave语言亲和度偏向后端开发团队偏向脚本与数据团队与 Selenium 配合成熟方案多同样成熟与 pytest 生态整合不沾边天然整合并行执行能力JUnit Platform 支持良好pytest-xdist 成熟社区资料量相对更多少一些但在增长2.2 接口自动化与 Selenium 怎么搭进 Cucumber很多团队误以为 Cucumber 是“UI 自动化框架”其实它根本不在乎你测的是页面还是接口。层抽象可以这样理解Cucumber 负责语义匹配 | -- Selenium WebDriver 处理 Web UI 操作 -- RestAssured 处理 HTTP API 请求 -- JDBC 处理数据库准备与校验接口自动化尤其适合 Cucumber。比如支付回调、鉴权这类后端场景用 Gherkin 描述“当第三方平台发送支付成功通知系统应生成订单流水”比直接甩一串 JSON 断言清晰得多。我见过一个团队把所有下单接口场景全用 Cucumber 管理需求变更时先改 Feature 再改代码回归周期从半天压缩到十分钟。UI 层面则用 Selenium 写 Step真正驱动浏览器干活。2.3 依赖引入与版本搭配建议如果你走 Java Maven 路线最小依赖组合是cucumber-javacucumber-junit-platform-enginejunit-platform-suite。这三个缺一不可cucumber-java提供Given注解junit-platform-engine让 Cucumber 跑在 JUnit 5 平台上junit-platform-suite是 Runner 的底座。properties cucumber.version7.18.1/cucumber.version junit.platform.version1.10.2/junit.platform.version /properties dependencies dependency groupIdio.cucumber/groupId artifactIdcucumber-java/artifactId version${cucumber.version}/version scopetest/scope /dependency dependency groupIdio.cucumber/groupId artifactIdcucumber-junit-platform-engine/artifactId version${cucumber.version}/version scopetest/scope /dependency dependency groupIdorg.junit.platform/groupId artifactIdjunit-platform-suite/artifactId version${junit.platform.version}/version scopetest/scope /dependency /dependencies这里有个容易踩的版本坑cucumber-junit-platform-engine对 JUnit Platform 版本有最低要求你单独升junit-platform-suite到较高版本时要确认与 Cucumber 内部依赖的junit-platform-engine没冲突。最省心的方法是让 Cucumber 父 POM 管住 JUnit 相关版本只在 properties 里覆盖 Cucumber 版本。3. 从零搭一个能跑的 Cucumber 工程依赖、目录、第一个场景3.1 Maven 标准目录Feature 文件和步骤定义的分工Cucumber 工程的目录结构有约定但理解约定背后的逻辑比记住路径更重要。Feature 文件放在src/test/resources/features因为它是资源文件测试代码打 jar 包时要带上Step Definitions 放在src/test/java下的包内Runner 通过glue参数指定包路径来关联。src/test ├── java │ └── com/example/demo │ ├── runner │ │ └── TestRunner.java │ └── stepdefs │ └── LoginStepDefs.java └── resources └── features └── login └── login.feature新手最容易犯的错是把 feature 文件放错目录或放错包结构导致 Runner 跑起来提示 “No features found at ...”。遇到这个报错先检查路径八成是资源目录不对。3.2 写第一个 Gherkin 场景用业务语言描述需求Feature 文件本质上是一份“可执行的验收标准”。写的时候有一条铁律只写用户视角能懂的话不要写任何技术细节。比如“用户输入正确的用户名和密码”可以“用户调用 POST /api/login 接口传参 usernamexxx 并断言 status 200”就不合格——后者是测试实现该关心的不是业务语义该关心的。Feature: 用户登录 Scenario: 使用正确凭证能够登录成功 Given 用户访问登录页面 When 用户输入用户名 demo 和密码 123456 And 用户点击登录按钮 Then 用户应该看到文本 欢迎回来注意几点细节字符串参数用双引号包裹Cucumber 表达式会自动提取And等价于上一步的类型延续不用纠结它是 Given 还是 When中文写 Feature 完全没问题IDE 插件能识别不过文件名建议英文部分报告插件对中文文件名支持不友好。3.3 Step Definitions 与 Runner让自然语言落到代码上写完 Feature 直接跑默认结果是一堆 Undefined Step。这不是失败而是你还没有给每个句子写实现。Step Definitions 是用Given/When/Then注解标注的 Java 方法Cucumber 靠正则或 Cucumber Expression 做全文匹配。package com.example.demo.stepdefs; import io.cucumber.java.en.Given; import io.cucumber.java.en.When; import io.cucumber.java.en.Then; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; public class LoginStepDefs { private WebDriver driver; Given(用户访问登录页面) public void openLoginPage() { driver new ChromeDriver(); driver.get(https://example.com/login); } When(用户输入用户名 {string} 和密码 {string}) public void inputCredentials(String username, String password) { driver.findElement(By.id(username)).sendKeys(username); driver.findElement(By.id(password)).sendKeys(password); } When(用户点击登录按钮) public void clickLoginButton() { driver.findElement(By.id(loginBtn)).click(); } Then(用户应该看到文本 {string}) public void verifyText(String expected) { String bodyText driver.findElement(By.tagName(body)).getText(); org.junit.jupiter.api.Assertions.assertTrue( bodyText.contains(expected), 页面应包含文本: expected); driver.quit(); } }Runner 类是入口。JUnit 5 Cucumber 7 的写法如下package com.example.demo.runner; import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; Suite IncludeEngines(cucumber) SelectClasspathResource(features/login) public class TestRunner { }跑完会得到一屏 JUnit 格式的通过/失败统计。到这一步一个最基础的 Cucumber 工程就跑通了。你会发现整个流程跟“先写需求再实现需求”几乎一一对应写自动化变成了翻译自然语言的过程而不是上来就堆代码。4. 过了“能跑”这关之后运行策略、并行与报告4.1 三种启动方式IDE、命令行、CI 流水线工程搭好之后日常使用会面临三种运行方式。IDE 里直接右键 RunTestRunner适合调试命令行mvn clean test适合本地验证CI 里mvn verify配套流水线最常用。Cucumber 对这三种方式天然支持不需要额外配置。我个人的习惯是写单个 Feature 阶段用 IDE 跑指定SelectClasspathResource到具体文件或直接改 tags提交代码前在命令行跑一次完整回归。CI 上则叠加 Maven Surefire 报告把失败信息及时暴露在流水线里。曾有同事直接在 Jenkins 里点构建不看控制台输出等发现红了一大片已经是三小时后——这个习惯要改Cucumber 的失败信息在控制台里其实非常清晰标明了哪个 step 失败、对应 feature 第几行。4.2 并行执行提速的关键也是线程事故的重灾区回归用例到几百条时单线程跑不动了。Cucumber 7 支持通过junit-platform.properties开启并行cucumber.execution.parallel.enabledtrue cucumber.execution.parallel.config.strategyfixed cucumber.execution.parallel.config.fixed.parallelism4配置本身不难难在并行之后 WebDriver 的状态管理。如果 Step Definitions 里用的是全局单例WebDriver并行一开就是串话用例 A 打开了登录页用例 B 却拿同一个 driver 去点了支付按钮。正确做法是用ThreadLocalWebDriver配合 WebDriverManager 按线程管理驱动生命周期。这是我见过 Cucumber 项目里最隐蔽也最致命的坑。public class DriverFactory { private static final ThreadLocalWebDriver DRIVER new ThreadLocal(); public static WebDriver getDriver() { if (DRIVER.get() null) { DRIVER.set(new ChromeDriver()); } return DRIVER.get(); } public static void closeDriver() { if (DRIVER.get() ! null) { DRIVER.get().quit(); DRIVER.remove(); } } }场景隔离也是必须考虑的。每个Scenario结束后确保清理数据并关闭 driver才能避免上一个场景的状态泄漏到下一个。4.3 报告给团队看的不是代码是业务执行记录很多人以为 Cucumber 的报告就是控制台那几行统计数据其实它最有价值的产物是 HTML 报告。用maven-cucumber-reporting插件可以生成图文并茂的报告页面包含通过率、失败步骤截图、执行耗时等。首先在 Runner 上配置生成 JSON 中间产物ConfigurationParameter(key PLUGIN_PROPERTY_NAME, value json:target/cucumber-reports/cucumber.json)然后在 pom 里挂 Surefire 的 post-phase 执行报告生成plugin groupIdnet.masterthought/groupId artifactIdmaven-cucumber-reporting/artifactId version5.7.8/version executions execution idgenerate-cucumber-reports/id phaseverify/phase goals goalgenerate/goal /goals configuration jsonFiles paramtarget/cucumber-reports/cucumber.json/param /jsonFiles outputDirectorytarget/cucumber-reports/outputDirectory /configuration /execution /executions /plugin报告最终要共享给产品、开发和测试一起看。我的经验是把报告地址贴进迭代周报让业务同学自己去看哪些场景没通过而不是等测试去口头解释。Feature 文件本身就是活文档加一个 CI 报告入口整个团队就拥有了可持续维护的验收记录。5. 真实项目里踩过的坑这些不解决Feature 写得再漂亮也白搭5.1 中文乱码环境配置能坑掉整个报告中文环境做 Cucumber 自动化乱码问题几乎是人人都要打的怪。具体表现是 Feature 里的中文在控制台或报告中变成????断言里的中文实际比对失败。根因通常是 Maven Surefire 没有显式指定 UTF-8。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.2.5/version configuration argLine-Dfile.encodingUTF-8/argLine /configuration /pluginJava 编译阶段同样建议显式指定编码properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding project.reporting.outputEncodingUTF-8/project.reporting.outputEncoding /properties很多团队卡在“用例在 IDE 里跑是好的到 CI 上中文就乱”十有八九是 Surefire 的argLine丢了。这个配置先加进 pom能省掉大量无意义的排查时间。5.2 等待策略无脑 sleep 是隐形炸弹Selenium 自动化里最常见的失败就是元素还没渲染就被定位。新手上路最喜欢Thread.sleep(2000)改一次跑一次越跑越慢越慢越改。Cucumber Step 本身有执行超时和重试机制但不解决等待问题。我建议三步走能定位就定位定位不到就用 WebDriverWait 显式等待再不行才考虑隐式等待兜底。WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.elementToBeClickable(By.id(loginBtn)));一个实际的教训线上环境偶发请求慢睡眠 3 秒时大概率通过但网络抖动时照样挂换成显式等待后用例不再依赖固定的停顿单条用例耗时还平均降了 1.8 秒。等待策略不是优化项是稳定性项目里的第一优先级。5.3 Hook 和步骤隔离Before/After 的“副作用”Cucumber 的Before、After与 JUnit 的注解名字相同但语义完全不同。Cucumber 的 Hook 是面向场景的每个 Scenario 执行前跑Before执行后跑After。这并不是坏事但如果 Hook 里做了创建数据库连接、清空缓存表、启动浏览器等耗时操作整个测试套件的性能会肉眼可见地下降。我踩过的坑是为了登录方便在Before里把所有公共数据都初始化了一遍后来场景变多每条用例启动都被拖到四五秒一个 200 条场景的套件跑出近二十分钟。把公共离线数据放到BeforeAll级别只在针对性的场景里单独建数据时间直接砍半。Hook 的使用原则应该是“越轻越好能不用就不用”。5.4 Step 匹配失败ambiguous steps 与正则陷阱Cucumber 里最折磨人的报错之一是AmbiguousStepException——多个 Step Definitions 匹配同一个句子Cucumber 不知道该挑哪个执行。出现这种问题的原因通常是正则写得太宽泛When(用户输入用户名 {string} 和密码 {string})和When(用户输入用户名 {string} 和手机号 {string})如果两个句子文本没有足够区分度Cucumber 表达式推进会报二义性。解决办法是句子写得具体到业务动作比如“用户使用账号密码登录”和“用户使用手机验证码登录”字面上先分开。正则技巧再高都不如业务语义清晰省事。6. 进阶玩法从“能用”升级到“好用”的实战习惯6.1 Scenario Outline一套逻辑验证多组数据真实项目中往往要多组数据验证同一个规则比如登录成功、密码错误、账号锁定。为每组数据写一个完整 Scenario 显得很啰嗦Scenario Outline就是为这种场景设计的。Feature: 登录规则校验 Scenario Outline: 登录结果校验 Given 用户访问登录页面 When 用户输入用户名 username 和密码 password And 用户点击登录按钮 Then 用户应该看到 expected Examples: | username | password | expected | | demo | 123456 | 欢迎回来 | | demo | wrong | 用户名或密码错误 | | locked | 123456 | 账号已被锁定 |这份文件读起来像一个需求表格业务方完全能看懂。测试数据与步骤实现分离后新增一组数据只需要在Examples表格加一行代码零改动。这个能力是普通 JUnit 参数化测试替代不了的——参数化的是某段代码而这里参数化的是整个业务场景。6.2 DataTable多实体数据传入一步到位某些场景需要批量准备数据比如下单时要同时传商品、地址、优惠券信息。Cucumber 的 DataTable 可以像表格一样传参Given 用户准备购买以下商品 | 商品 | 单价 | 数量 | | 键盘 | 129 | 1 | | 鼠标 | 89 | 2 |Step 里用DataTable接收并做列表映射Given(用户准备购买以下商品) public void prepareCart(DataTable dataTable) { ListMapString, String rows dataTable.asMaps(String.class, String.class); for (MapString, String row : rows) { System.out.println(row.get(商品) row.get(单价) row.get(数量)); } }DataTable 适合两件事批量页面录入、批量接口参数组装。相比写死一组组参数表格形式更直观后续维护数据的人不需要懂代码。6.3 Tags让同一套用例按需拆成多套执行计划Cucumber 的 Tag 机制用来划分执行子集非常顺手。我在项目里通常定义三类smoke冒烟集、critical核心链路、slow慢用例。Runner 里用 Tag 表达式过滤。ConfigurationParameter(key FILTER_TAGS_PROPERTY_NAME, value smoke and not slow)这样开发本地跑smokeCI 回归跑critical定时任务跑全量。Tag 表达式的语法支持and/or/not写起来像条件查询。一个明智的做法是需求上线前只跑smoke加critical快速给出通过结论全量回归放到深夜定时任务配合报告告警。把什么用例放在哪个 Tag 下本身就是测试策略的体现。6.4 把 Feature 文件当成需求的活文档来维护讲到最后说点跟“自动化测试框架”本身关系不大、但影响非常大的事情Feature 文件的维护节奏。很多团队的 Feature 文件是上线前突击写完的之后需求一改用例就腐化最后没人跑、没人更新Cucumber 形同虚设。我现在的习惯是需求迭代开始时新建 Feature 文件和开发同步更新需求评审时用 Feature 文件作为讨论底稿提测阶段 Feature 里描述的验收场景就是测试用例。需求有变先改 Feature再改实现。这样能保证 Feature 文件永远是最新、最可读的验收文档自动化测试框架才真正变成团队的通用语言。最后分享一个我自己的习惯现在每接一个新需求我第一件事不是写代码而是先打开 Cucumber把 Given/When/Then 按业务描述出来发给产品和开发确认。确认的过程比写代码的过程更能暴露需求理解偏差。这个习惯帮我减少了很多返工——有一次产品看了 Gherkin 才发现自己原始需求漏了一个“未登录状态也要允许加入购物车”的规则当场补上了。后来我越发觉得Cucumber 最大的价值不在测试本身而在它逼着团队在写代码之前先把“系统该干什么”这件事说清楚。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询