用 Cucumber 把行为示例推进为可执行测试

用 Cucumber 把行为示例推进为可执行测试

来源:Cucumber 官方 10-minute tutorial,Java 分支。中文翻译与技术整理:未完纪。2026-10-08 读取;页面最后更新标为 2026-10-08。

Cucumber反馈循环示意:Gherkin场景先变成undefined,再补步骤定义变成pending,接通业务与断言产生失败,最小实现通过,随后添加新示例与重构。
图:行为示例到可执行测试的反馈循环,未完纪原创;通过与失败表示教程阶段,不是本轮测试日志。

Cucumber 用具体的业务示例来表达软件应该如何工作。场景先于生产代码写出,最初是可执行规格;随着实现逐渐完成,它们也成为持续更新的文档与自动化测试。本教程用一个很小的库回答“今天是不是星期五”,让读者看到从未定义步骤到绿色结果的整个反馈过程。

本文按研究页范围整理 Java 分支。需要基本 Java 知识、终端与编辑器使用经验、Java SE,以及 Maven 3.3.1 或更高版本。原文以 IntelliJ IDEA 和 Cucumber for Java 插件为例,也列出 Eclipse 及相应插件作为选择。IDE 插件便于编辑,不应替代构建工具自身的测试结果。

全文来源:Cucumber 官方 10-minute tutorial。源页当前使用 cucumber-archetype 8.0.4;研究页此前记录为 8.0.3。本稿采用本次读取的 8.0.4,并记录这个版本变化。所有预期输出均属于原教程,本轮未执行 Maven 或测试。

创建最小项目并验证工具链

在用于练习的空目录中创建项目。下列命令保留原文 Bash 风格的反斜杠续行;PowerShell 用户应写成一行或改用其对应续行方式。Maven 会读取依赖并可能联网下载插件,需在自己的受控开发环境运行。

mvn archetype:generate \
  "-DarchetypeGroupId=io.cucumber" \
  "-DarchetypeArtifactId=cucumber-archetype" \
  "-DarchetypeVersion=8.0.4" \
  "-DgroupId=hellocucumber" \
  "-DartifactId=hellocucumber" \
  "-Dpackage=hellocucumber" \
  "-Dversion=1.0.0-SNAPSHOT" \
  "-DinteractiveMode=false"

cd hellocucumber
mvn test

在 IntelliJ IDEA 中,可用 File → Open 选择 pom.xml,再选择以项目打开。源文在初次 mvn test 后显示构建成功但 Tests run: 0;它表示尚未找到可运行场景,不能据此宣称业务已经通过测试。以本次生成目录和 pom.xml 为准,原文日志中的目录名称与具体耗时只是示意。

先写一个可以讨论的场景

团队可以先用 Example Mapping 一起讨论例子,再把共同语言写入场景。创建 src/test/resources/hellocucumber/is_it_friday_yet.feature:

Feature: Is it Friday yet?
  Everybody wants to know when it's Friday

  Scenario: Sunday isn't Friday
    Given today is Sunday
    When I ask whether it's Friday yet
    Then I should be told "Nope"

Feature 后面是功能名称,描述行是给人看的文字,不执行。Scenario 是具体行为例子:先给出前提 Given,再描述动作 When,最后用 Then 表达可观察结果。正文解释使用中文,步骤里的英文保持一致,避免只翻译 feature 而忘了同步 Java 表达式。

从 undefined 到 pending

再次运行 mvn test 时,Cucumber 能找到这个场景,但还不知道三句步骤对应什么代码。源教程报告一个 undefined 场景和三个 undefined 步骤,并给出步骤定义片段。把它们加入 src/test/java/hellocucumber/StepDefinitions.java,并放在对应包与类中:

package hellocucumber;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;

public class StepDefinitions {
    @Given("today is Sunday")
    public void today_is_sunday() {
        throw new io.cucumber.java.PendingException();
    }

    @When("I ask whether it's Friday yet")
    public void i_ask_whether_it_s_friday_yet() {
        throw new io.cucumber.java.PendingException();
    }

    @Then("I should be told {string}")
    public void i_should_be_told(String expectedAnswer) {
        throw new io.cucumber.java.PendingException();
    }
}

这里的 PendingException 是教程故意留下的待实现标记。源教程再次运行后是一个 pending 场景,三步中一步 pending、后续两步 skipped。它比 undefined 更进一步:Cucumber 已经找到步骤定义并开始调用,只是步骤还没有真实行为。

接通业务调用,让断言真正失败

下一步给步骤赋予含义:Given 保存 today,When 调用业务方法,Then 对结果断言。尽量保持业务讨论、场景与代码使用同一组词汇,让日后维护者能直接对应它们。

package hellocucumber;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import static org.assertj.core.api.Assertions.assertThat;

class IsItFriday {
    static String isItFriday(String today) {
        return null;
    }
}

public class StepDefinitions {
    private String today;
    private String actualAnswer;

    @Given("today is Sunday")
    public void today_is_Sunday() {
        today = "Sunday";
    }

    @When("I ask whether it's Friday yet")
    public void i_ask_whether_it_s_Friday_yet() {
        actualAnswer = IsItFriday.isItFriday(today);
    }

    @Then("I should be told {string}")
    public void i_should_be_told(String expectedAnswer) {
        assertThat(actualAnswer).isEqualTo(expectedAnswer);
    }
}

源教程的结果变成一个 failed 场景:前两步通过,最后一步发现 expected 为 ‘Nope’,实际为 null。这才是业务差异造成的失败,区别于步骤未定义或尚未实现。不要为了“全绿”直接删除断言;断言把可执行步骤与预期行为连接起来。

最小实现通过后,增加第二个失败例

为满足现有的“星期日不是星期五”场景,最小实现只需返回 ‘Nope’。替换业务方法后,原教程显示一个场景、三步通过。这一步只是满足当前示例,并未实现所有星期的逻辑。

static String isItFriday(String today) {
    return "Nope";
}

接着在 feature 中加入星期五场景,并为它提供 Given。此时固定返回 ‘Nope’ 的实现应该暴露问题:星期日场景仍通过,星期五场景期望 ‘TGIF’ 却得到 ‘Nope’。

  Scenario: Friday is Friday
    Given today is Friday
    When I ask whether it's Friday yet
    Then I should be told "TGIF"
@Given("today is Friday")
public void today_is_Friday() {
    today = "Friday";
}

现在再实现判断,而不是提前猜测所有需求:

static String isItFriday(String today) {
    return "Friday".equals(today) ? "TGIF" : "Nope";
}

源教程随后显示两个场景、六步通过。这些数字描述原教程的反馈顺序,本稿没有重新运行;日志里的耗时以及部分仍引用 skeleton/belly 的堆栈片段也不当作本次环境证据。

用场景大纲让规则覆盖多个具体输入

一周不只有星期日与星期五。场景的动作和断言相同,变化的是输入和预期,可以把 Scenario 改为 Scenario Outline,并用 Examples 表列出样本。替换 feature 的具体场景为:

Feature: Is it Friday yet?
  Everybody wants to know when it's Friday

  Scenario Outline: Today is or is not Friday
    Given today is "<day>"
    When I ask whether it's Friday yet
    Then I should be told "<answer>"

    Examples:
      | day            | answer |
      | Friday         | TGIF   |
      | Sunday         | Nope   |
      | anything else! | Nope   |

再把星期日与星期五的两个 Given 替换为一个接受字符串的表达式。下面是可读的最终合并版本。原文最后一段把 public 类名写成 Stepdefs,而前文要求文件名为 StepDefinitions.java;这里明确修正为 StepDefinitions,保持 Java 公共类与文件名一致。

package hellocucumber;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import static org.assertj.core.api.Assertions.assertThat;

class IsItFriday {
    static String isItFriday(String today) {
        return "Friday".equals(today) ? "TGIF" : "Nope";
    }
}

// 编辑修正:原文此处为 Stepdefs;与 StepDefinitions.java 统一。
public class StepDefinitions {
    private String today;
    private String actualAnswer;

    @Given("today is {string}")
    public void today_is(String today) {
        this.today = today;
    }

    @When("I ask whether it's Friday yet")
    public void i_ask_whether_it_s_Friday_yet() {
        actualAnswer = IsItFriday.isItFriday(today);
    }

    @Then("I should be told {string}")
    public void i_should_be_told(String expectedAnswer) {
        assertThat(actualAnswer).isEqualTo(expectedAnswer);
    }
}

Cucumber 为表中每一行具体化一个场景,把 day 与 answer 分别代入步骤;Java 的 {string} 接收引号内的值。原文最终展示三个场景、九步通过。三个例子仅覆盖它们明确表达的行为,不构成完整测试覆盖率证明。当前业务方法也只是精确比较字符串 ‘Friday’,没有解析真实日期、语言、时区或大小写。

在保留行为的前提下重构

原文最后要求把 isItFriday 从测试代码移入生产代码,并按需要提取多个步骤共同使用的辅助方法。在这个项目中,可以把 IsItFriday 放入 src/main/java/hellocucumber/IsItFriday.java,包名保持 hellocucumber;步骤类仍留在测试目录。重构后再次运行场景,是检查行为是否保持的一种方式,不要把这一步省略成单纯移动文件。

这次教程的价值是不断用例子约束实现:发现无定义、接上步骤、建立有效失败、写最小实现、增加反例、参数化,再重构。它没有覆盖 Web、数据库或完整业务系统,不应让几个绿色场景代替真实系统的安全、异常与集成测试。

静态审核与许可

已静态检查文件位置、注解表达式、状态保存、断言和公共类命名。未见硬编码秘密、动态代码执行、注入入口或破坏性命令。Maven 插件与依赖解析会执行构建逻辑并可能联网,这些命令本轮均未运行。修正类名和按当前页更新 archetype 版本不等于已证明能在任意 JDK/构建配置中通过。

© 2014–2026 The Cucumber Open Source Project。源文未单列个人作者;官方文档源文件所在仓库采用 MIT License,版权为 Aslak Hellesøy and contributors。本文下方附有完整许可文本;译文、类名勘误和原创示意图由未完纪整理。

MIT License 完整文本(Cucumber 网站仓库)
MIT License

Copyright (c) 2014 Aslak Hellesøy and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容