Cucumber-js 完整实战指南:在 Node.js 中用自然语言编写可执行的自动化测试

  • 测试
  • 开发工具

【免费下载链接】cucumber-js

Cucumber for JavaScript

项目地址: https://gitcode.com/gh_mirrors/cu/cucumber-js
点击查看 免费下载

Cucumber-js 是 Cucumber 官方 JavaScript 实现,它让你用团队任何人都能读懂的"自然语言"(Gherkin 语法)编写自动化测试,同时用 JavaScript 编写对应的步骤定义(Step Definitions),把"需求描述"和"测试代码"无缝连接起来。本指南将以官方 README 的快速上手路径为主线,完整覆盖从安装、编写第一个 Feature 到配置、执行、报告、并行与扩展的实战全流程,读完即可在本地项目里落地一套 BDD 测试体系。

什么是 Cucumber-js:面向团队协作的行为驱动测试

Cucumber 的核心思想是:用自然语言编写自动化测试。因为这些测试用自然语言写成,团队里的任何人都能读懂;因为人人都能读懂,它们就能帮助改善团队的沟通、协作与信任——产品、开发、测试可以围绕同一份"可执行的规格说明"对齐。

Cucumber-js 是 Cucumber 的 JavaScript 实现,运行在 Node.js 的受维护版本 目录下也存放了大量覆盖各功能的 .feature 验收测试,是绝佳的参考样本。

从仓库结构看,Cucumber-js 的架构大体分为几层:

  • src/api:对外暴露的 JavaScript API(加载配置、加载源码、执行测试运行);
  • src/configuration:配置文件解析、合并、校验与默认值;
  • src/support_code_library_builder:收集你注册的步骤定义、Hooks、参数类型与 World;
  • src/runtime:串行/并行执行测试用例的运行时;
  • src/formatter:内置的各种输出格式(progress、summary、html、message 等)。

安装与常见坑

Cucumber-js 发布在 npm 上,包名为 @cucumber/cucumber(注意不是旧的 cucumber)。使用 npm 安装:

npm install @cucumber/cucumber

也可以使用 Yarn:

yarn add @cucumber/cucumber

安装后会提供 cucumber-js 可执行文件(见 package.json 中 bin 字段指向 bin/cucumber.js)。当前版本要求 Node.js 22 || 24 || >=26(见 package.json 的 engines 字段)。

无效安装:多实例问题

Cucumber 与其他多数库不同,它是有状态(stateful)的:你调用 Given/When/Then 等函数来注册支持代码(support code),这些状态会一直保留到测试运行时使用。因此,项目里所有与 Cucumber 交互的代码必须操作同一个实例。如果出现类似下面的报错:

You're calling functions (e.g. "Given") on an instance of Cucumber that isn't running.
This means you have an invalid installation, mostly likely due to:
...

说明存在无效安装。常见成因与解法(详见 docs/installation.md):

  • 全局安装:Cucumber 不适合全局安装,必须作为项目依赖。若在 debug 模式 下检测到全局安装,会发出警告。
  • 重复依赖:项目中某个第三方库自身也依赖了 @cucumber/cucumber(可能版本不同),导致多个实例同时存在——这与 React 的 "invalid hook call" 问题类似。可用 npm why @cucumber/cucumber 诊断。解决方式是把共享库中对 @cucumber/cucumber 的依赖改为 peer dependency;若库不受你控制,可用 npm 的 overrides 或 Yarn 的 resolutions 收敛到单一实例。
  • 废弃包:检查是否有代码依赖了旧的、已废弃的 cucumber 包,所有 Cucumber 相关代码都应改用 @cucumber/cucumber(参考 UPGRADING.md)。
  • Linking:即使正确使用了 peer dependency,开发时用 npm link/yarn link 关联本地共享库也可能触发该问题,可从宿主项目执行 npm link ../my-project/node_modules/@cucumber/cucumber 绕过。

早期版本中该问题表现为更隐晦的错误(原因与解法相同):

TypeError [ERR_INVALID_ARG_TYPE]: The "from" argument must be of type string. Received type undefined
    at validateString (internal/validators.js:125:11)
    ...

快速上手:从零写出第一个通过的测试

官方 README 用一个经典的 Greeter 例子演示了完整的 BDD 流程:先写业务代码,再写 Feature(自然语言),再实现步骤定义,最后运行。

第一步:编写业务代码 src/index.js

class Greeter {
  sayHello() {
    return 'hello'
  }
}

module.exports = {
  Greeter
}

第二步:编写 Feature 文件 features/greeting.feature

Feature: Greeting

  Scenario: Say hello
    When the greeter says hello
    Then I should have heard "hello"

第三步:实现步骤定义 features/support/steps.js

const assert = require('assert')
const { When, Then } = require('@cucumber/cucumber')
const { Greeter } = require('../../src')

When('the greeter says hello', function () {
  this.whatIHeard = new Greeter().sayHello()
})

Then('I should have heard {string}', function (expectedResponse) {
  assert.equal(this.whatIHeard, expectedResponse)
})

第四步:运行

npx cucumber-js

运行成功后的终端输出大致如下(1 个场景、2 个步骤全部通过):

终端输出:1 个场景、2 个步骤全部通过的测试运行结果

这个例子背后发生的事值得拆解:npx cucumber-js 启动 CLI(入口为 src/cli/run.ts)→ 按默认 glob features/**/*.{feature,feature.md} 找到 feature 文件 → 由 Gherkin 解析器把场景编译为 Pickle → 加载 features/support/** 下的支持代码 → 用 src/runtime 逐场景执行。若你更喜欢看完整示例工程,官方维护了 cucumber-js-examples 示例仓库可参考。

核心概念:Feature、步骤定义、World 与 Hooks

快速上手之后,你需要理解支撑 Cucumber 的四个核心概念。

Feature 文件与 Gherkin

Feature 文件以 .feature 或 .feature.md 结尾,默认放在 features/ 目录,内容使用 Gherkin 语法(Feature、Scenario、Given/When/Then/And/But 以及 Scenario Outline、Rule 等)。仓库自己的验收测试(如 features/core.feature、features/rule.feature)就是真实可运行的 Gherkin 示例。Cucumber-js 还支持通过 language 配置项指定非英语方言(对应 CLI --language 选项,默认 en)。

步骤定义:Cucumber Expressions 与正则表达式

步骤定义(Given/When/Then)是 Gherkin 文本与实际测试实现之间的"胶水"。Cucumber 支持两种匹配表达式(完整接口见 docs/support_files/step_definitions.md)。

Cucumber Expressions(推荐):

Given I have 42 cucumbers in my belly
import { Given } from '@cucumber/cucumber';

Given('I have {int} cucumbers in my belly', function (cucumberCount) {
  assert.equal(this.responseStatus, cucumberCount)
});

{int} 会把捕获到的字符串自动转换成数字传入步骤函数。除 {int} 外还有内置的 float、string(自动去除引号)、word 等参数类型,你也可以用 defineParameterType 自定义(见下文 API 参考)。

正则表达式:正则中的捕获组会按顺序作为参数传给步骤函数:

const { Then, When } = require('@cucumber/cucumber');
const assert = require('assert');
const fs = require('fs');
const seleniumWebdriver = require('selenium-webdriver');

// 同步
Then(/^the response status is (.*)$/, function (status) {
  assert.equal(this.responseStatus, status)
});

// 异步 - 回调风格:最后一个参数是 callback,完成后调用
Then(/^the file named (.*) is empty$/, function (fileName, callback) {
  fs.readFile(fileName, 'utf8', function(error, contents) {
    if (error) {
      callback(error);
    } else {
      assert.equal(contents, '');
      callback();
    }
  });
});

// 异步 - Promise 风格:返回 Promise,resolve/reject 即步骤完成
When(/^I view my profile$/, function () {
  return this.driver.findElement({ css: '.profile-link' }).then(function(element) {
    return element.click();
  });
});

步骤定义函数支持同步、回调、Promise 三种接口;箭头函数无法以 this 引用 World,需要改用 world 代理对象(详见下文 World 一节)。

World:每个场景的隔离上下文

World 是每个场景独立的隔离作用域,通过 this 暴露给步骤与大部分 Hooks。你可以在一个步骤里设置变量,在后面的步骤里读取;场景结束时所有变量随之丢弃。每个场景开始时都会创建全新的 World 实例,即使该场景被重试(Retry)也会重新创建。BeforeAll/AfterAll 运行于任何场景之外,因此拿不到 World。

const { Given, Then } = require('@cucumber/cucumber')

Given("my color is {string}", function(color) {
  this.color = color
})

Then("my color should not be red", function() {
  if (this.color === "red") {
    throw new Error("Wrong Color");
  }
});

内置的 World 类会向构造函数传入 attach(附件)、log(日志)、link(链接)、parameters(世界参数)等帮助方法,默认绑定到 this 上(详见 docs/support_files/world.md)。你可以通过 setWorldConstructor 传入自定义 World 类(推荐继承内置 World),并利用 worldParameters 配置项注入 appUrl、browser 之类的运行参数:

  • 配置文件 { worldParameters: { appUrl: 'http://localhost:3000/' } }
  • CLI cucumber-js --world-parameters '{"appUrl":"http://localhost:3000/"}'

从 v10.8.0 起,箭头函数中可以用导出的 world 代理对象访问 World;从 v10.1.0 起 BeforeAll/AfterAll 也能通过 this.parameters 读取世界参数(详见 docs/support_files/world.md)。

Hooks:场景生命周期钩子

Hooks 用于在每个场景前后做环境搭建与清理(详见 docs/support_files/hooks.md)。多个 Before 按定义顺序执行,多个 After 按定义顺序的逆序执行。支持同步、回调、Promise 三种风格:

const {After, Before} = require('@cucumber/cucumber');

// 同步
Before(function () {
  this.count = 0;
});

// 异步回调
Before(function (testCase, callback) {
  var world = this;
  tmp.dir({unsafeCleanup: true}, function(error, dir) {
    if (error) {
      callback(error);
    } else {
      world.tmpDir = dir;
      callback();
    }
  });
});

// 异步 Promise
After(function () {
  return this.driver.quit();
});

Hooks 支持丰富的能力:

  • 命名 Hooks(v8.1.0+):Before({name: "Set up some test state"}, function () {...}),名称会出现在 formatter 输出中便于理解测试;
  • 标签化 Hooks:Before({tags: "@foo and @bar"}, function () {...}),用 tag expressions 按场景标签条件执行,也支持字符串简写 Before("@foo", ...);
  • 跳过场景:在 Before 中返回 'skipped'(或回调/Promise 解析为 'skipped')即可命令式跳过后续场景;
  • BeforeAll / AfterAll:所有场景前/后的全局设置,在并行模式下默认每个 worker 执行一次,也可用 {on: HookTarget.COORDINATOR} 让钩子只在协调进程执行一次(v13.2.0+,用于启动共享服务器、灌数据库等场景);
  • BeforeStep / AfterStep:每个步骤前后的钩子,同样支持标签条件,AfterStep 可通过 {result} 判断 Status.FAILED 并在失败时截图。

完整的 API 签名(含参数对象结构)见 docs/support_files/api_reference.md,所有方法都可以从 require('@cucumber/cucumber') 解构得到——这一点在 src/index.ts 中可以看到 After、Before、Given、setWorldConstructor、defineParameterType、setParallelCanAssign、Status、World、world 等导出都来自 src/support_code_library_builder 的 methods。

配置:配置文件与全部选项

Cucumber-js 会把"找 Feature、找支持代码、选格式、并行度"等行为集中到一份配置里(详见 docs/configuration.md)。

配置文件与查找顺序

Cucumber 会在项目根目录依次查找以下文件,使用第一个找到的:

  • cucumber.json
  • cucumber.yaml
  • cucumber.yml
  • cucumber.js
  • cucumber.cjs
  • cucumber.mjs

也可以用 --config 指定其他位置:

cucumber-js --config config/cucumber.json

JSON 格式示例:

{
  "default": {
    "parallel": 2,
    "format": ["html:cucumber-report.html"]
  }
}

YAML 格式示例:

default:
  parallel: 2
  format:
    - "html:cucumber-report.html"

JavaScript(ESM)格式示例:

export default {
  parallel: 2,
  format: ['html:cucumber-report.html']
}

JavaScript(CommonJS)格式示例:

module.exports = {
  default: {
    parallel: 2,
    format: ['html:cucumber-report.html']
  }
}

Cucumber 也支持把配置写成 CLI 风格的字符串(不推荐):

module.exports = {
  default: '--parallel 2 --format html:cucumber-report.html'
}

配置项之所以放在 default 键下,是为了支持 Profiles(配置组合)。此外,你还可以写 .ts/.mts/.cts 后缀的 TypeScript 配置文件,它由 Node.js 内置 TypeScript 支持加载(不会读取你的 tsconfig.json,类型导入需显式使用 import type):

import type { IConfiguration } from '@cucumber/cucumber'

export default {
  parallel: 2,
  format: ['html:cucumber-report.html']
} satisfies Partial<IConfiguration>

全部配置选项速查表

以下选项既可用于配置文件,也可用于 CLI,或两者同时使用(详见 docs/configuration.md):

名称类型可重复CLI 选项说明默认值
pathsstring[]是(作为参数)Feature 文件路径[]
backtraceboolean否--backtrace, -b显示错误的完整堆栈false
dryRunboolean否--dry-run, -d只准备测试运行但不执行false
forceExitboolean否--exit, --force-exit测试运行结束后显式调用 process.exit()false
failFastboolean否--fail-fast测试失败即停止false
formatstring[]是--format, -f每个要使用的 formatter 的名称/路径与(可选)输出文件[]
formatOptionsobject是--format-options传给 formatter 的选项{}
importstring[]是--import, -i支持代码路径(ESM 加载)[]
languagestring否--languageFeature 文件默认语言en
loaderstring[]是--loader, -l加载支持代码前注册的 loader 模块[]
namestring否--name按场景名称正则过滤[]
orderstring否--order按定义顺序或随机顺序运行defined
parallelnumber否--parallel用给定数量的 worker 并行运行0
pluginstring[]是--plugin要加载的插件路径或包名[]
pluginOptionsobject是--plugin-options传给插件的选项{}
publishboolean否--publish发布报告到 Cucumber Reportsfalse
requirestring[]是--require, -r支持代码路径(CommonJS 加载)[]
requireModulestring[]是--require-module用 require() 加载的转译模块名[]
retrynumber否--retry失败测试最多重试次数0
retryTagFilterstring是--retry-tag-filter可重试场景的标签表达式(空)
shardstring否--shard跨多次运行拆分场景子集(空)
strictboolean否--strict, --no-strict有 pending 步骤则测试运行失败true
tagsstring是--tags, -t过滤运行场景的标签表达式(空)
worldParametersobject是--world-parameters传给 World 的参数{}

规则:可重复选项多次提供时会被追加/合并;不可重复选项以 CLI 为准(CLI 优先于配置文件)。上述默认值可在 src/configuration/default_configuration.ts 中逐一印证(backtrace: false、dryRun: false、parallel: 0、strict: true、order: 'defined' 等)。

查找 Feature 与支持代码

默认情况下,Cucumber 按以下 glob 查找 Feature(相对项目根目录):

features/**/*.{feature,feature.md}

如需覆盖,可在配置中指定 paths 或直接在 CLI 传入 glob/目录:

  • 配置文件 { paths: ['somewhere-else/**/*.feature'] }
  • CLI cucumber-js 'somewhere-else/**/*.feature'(注意要加引号,避免 shell 自行展开 glob 导致行为不同)

支持代码(步骤定义等)默认逻辑是:

  • 若 Feature 位于 features 目录(任意层级):features/**/*.@(js|cjs|mjs)
  • 否则:为每个包含所选 Feature 的目录加载 <DIR>/**/*.@(js|cjs|mjs)

可用 import 选项覆盖(指定后默认规则不再生效,可重复、多值合并):

  • 配置文件 { import: ['somewhere-else/support/*.js'] }
  • CLI cucumber-js --import somewhere-else/support/*.js

import 走 Node.js 新版 ESM API 加载;require 走旧版 CommonJS API(例如与 require-module 转译配合时可能需要改用 require)。

CLI:运行方式与专属选项

安装 @cucumber/cucumber 后即可运行可执行文件(详见 docs/cli.md)。三种方式:

# 直接调用
./node_modules/.bin/cucumber-js
// 放进 package.json scripts
{
  "scripts": {
    "cucumber": "cucumber-js"
  }
}
# 用 npx
npx cucumber-js

所有标准配置选项都可以通过 CLI 提供,此外还有几个 CLI 专属选项:

选项类型可重复说明
--config, -cstring否配置文件路径
--profile, -pstring[]是要合并的配置 Profile
--version, -vboolean否打印当前版本后退出
--i18n-keywordsstring否打印指定 ISO-639-1 语言码的 Gherkin 关键字后退出
--i18n-languagesboolean否打印 Gherkin 支持的语言列表后退出

查看当前版本的全部选项:

cucumber-js --help

退出行为:默认 Cucumber 在事件循环清空后退出。若测试运行结束后进程仍挂起,可用 forceExit 强制关闭事件循环(配置文件 { forceExit: true },CLI cucumber-js --force-exit)。官方不鼓励依赖此选项,建议排查导致挂起的根因(可用 Node.js inspector、why-is-node-running 等工具定位)。

报告与反馈:内置 Formatters

Formatters 实时消费测试运行数据并输出到终端或文件(某些框架称之为 "reporters",详见 docs/formatters.md)。通过 format 配置项指定一个或多个格式:

  • 配置文件 { format: ['progress-bar', ['html', 'cucumber-report.html']] }
  • CLI cucumber-js --format progress-bar --format "html":"cucumber-report.html"

每个格式需要一或两个值:第一个(必填)用于标识 formatter,可以是内置 formatter 名称(如 progress-bar)、npm 包名(如 @cucumber/pretty-formatter)、本地实现相对路径(./my-custom-formatter.js)或 file:// 绝对路径;第二个(可选)是输出文件路径,若目录不存在会自动创建。不指定第二个值时输出到 stdout。CLI 中名称与路径用 : 分隔并各自加双引号;配置文件推荐用数组形式 ["html", "cucumber-report.html"]。

注意:多个 formatter 输出到同一目标时只有最后一个生效;若没有为 stdout 指定 formatter,默认使用 progress。

formatter 选项与主题

通过 formatOptions 传入(可重复、多次合并,后值优先)。终端 formatter(summary、progress、progress-bar、pretty)通用选项:

  • includeAttachments:设为 false 时不包含附件输出(默认 true)
  • theme:定制终端输出样式

theme 的键值大多是 Node.js util.styleText 可接受的样式名数组(bold、italic、red、magenta、bgYellow 等),每个键都可选,只覆盖想改的部分:

{
  "formatOptions": {
    "theme": {
      "affix": ["dim"],
      "attachment": ["cyan"],
      "dataTable": { "all": [], "border": ["dim"], "content": [] },
      "docString": { "all": [], "content": [], "delimiter": ["dim"], "mediaType": ["dim"] },
      "feature": { "all": [], "keyword": ["bold"], "name": [] },
      "location": ["dim"],
      "rule": { "all": [], "keyword": ["bold"], "name": [] },
      "scenario": { "all": [], "keyword": ["bold"], "name": [] },
      "status": {
        "all":      { "PASSED": ["green"], "FAILED": ["red"] },
        "icon":     { "PASSED": "✔", "FAILED": "✘" },
        "progress": { "PASSED": ".", "FAILED": "F" }
      },
      "step": { "argument": [], "keyword": ["bold"], "text": [] },
      "symbol": { "bullet": "•" },
      "tag": ["cyan"]
    }
  }
}

某些 formatter 有专属选项,需要放在与 formatter 同名的键下:{ formatOptions: { pretty: { useStatusIcon: false } } }。

颜色输出

内置 formatter 默认会输出彩色文本,Node.js 会检测底层流是否支持颜色。可用 FORCE_COLOR=1 强制开启、FORCE_COLOR=0 强制关闭(跨工具标准,也会影响断言库等其他工具)。旧的 colorsEnabled format 选项已废弃(见 docs/deprecations.md)。

内置 formatter 一览

  • summary:输出测试运行结果摘要。全部通过时简短干净;有问题时会给出失败 Hook/步骤的错误与堆栈、pending 步骤位置,以及实现未定义步骤的 Snippets。
  • progress:在结束时与 summary 相同,同时每个步骤/Hook 完成时给出简洁的实时反馈(./F 等字符)。
  • progress-bar:类似 progress,但提供基于待执行步骤总数的实时进度条。
  • pretty(v12.1.0+,也可作为 @cucumber/pretty-formatter 包引用):输出场景与示例执行过程的富文本报告,额外支持 includeFeatureLine、includeRuleLine、useStatusIcon 三个选项(默认均 true)。
  • html:生成独立可交互的 HTML 报告页面,可查看错误与堆栈、内联渲染附件、按状态过滤、按关键字或标签表达式搜索。默认附件以内嵌方式打包(文件完全自包含但可能很大),也可通过 formatOptions.html.externalAttachments 改为外置附件文件(支持 ["image/*", "video/*"] 这类模式,log/link 附件永不外置)。
  • message:输出整个测试运行的 Cucumber Messages(换行分隔的 JSON),可被其他工具消费,是结构化数据表示的首选。
  • json:输出旧版 JSON 格式报告。该 formatter 处于维护模式,不会新增功能,建议迁移到 message。
  • junit:输出 JUnit XML 格式,便于 CI 平台接入;可通过 formatOptions.junit.suiteName 设置 testsuite 的 name 属性(默认 cucumber-js)。
  • snippets:只打印实现未定义步骤的 Snippets,方便一次性收集待实现代码。
  • usage:列出步骤定义及其使用情况(含每次耗时与未使用的步骤):
┌───────────────────────────────────────┬──────────┬─────────────────────────────────┐
│ Pattern / Text                        │ Duration │ Location                        │
├───────────────────────────────────────┼──────────┼─────────────────────────────────┤
│ an empty todo list                    │ 760.33ms │ support/steps/steps.ts:6        │
│   an empty todo list                  │ 820ms    │ features/empty.feature:4        │
│ ...                                   │ ...      │ ...                             │
│ I remove the todo {string}            │ UNUSED   │ support/steps/steps.ts:33       │
└───────────────────────────────────────┴──────────┴─────────────────────────────────┘
  • usage-json:与 usage 相同但输出 JSON,可落盘给其他工具消费。

此外还有 rerun formatter(见下节 Rerun)与 rerun 文件机制配合使用。

测试选择与执行策略

过滤与排序

开发/调试时经常只想跑一个子集(详见 docs/filtering.md):

  • 按路径:{ paths: ['features/my_feature.feature'] } 或 cucumber-js features/my_feature.feature;也可以用 features/my_feature.feature:3 精确定位某一行(对应行内的场景)。可重复,多值合并。
  • 按名称:{ name: ['^start.+end$'] } 或 cucumber-js --name "^start.+end$";场景名特殊字符需转义(如 \(Scenario Name\))。可重复,命中任一表达式即运行。
  • 按标签:{ tags: '@foo or @bar' } 或 cucumber-js --tags "@foo or @bar",使用 Cucumber 标签表达式。可重复,多次提供会被 and 组合(场景需命中全部)。
  • 排序:order 默认为 defined(按发现顺序,大致为文件路径字母序 + 文件内顺序;显式传入多个 glob/path 时会遵守给定顺序);可设为 random 随机打乱,也可带种子如 random:234119。

并行执行(Parallel)

Cucumber 支持并行运行场景:主进程作为 "coordinator"(协调者)派生多个 worker 线程(详见 docs/parallel.md):

  • 配置文件 { parallel: 3 }
  • CLI cucumber-js --parallel 3

每个 worker 会额外收到 CUCUMBER_PARALLEL=true、CUCUMBER_TOTAL_WORKERS(worker 总数)、CUCUMBER_WORKER_ID(如 '0'/'1'/'2')三个环境变量。并行时 summary 最后一行会区分真实耗时与步骤累计耗时:

73 scenarios (73 passed)
512 steps (512 passed)
0m51.627s (executing steps: 4m51.228s)

worker 以线程形式运行在主进程内,可用 workerOptions 透传给 Node.js Worker 构造器(如设置资源上限):

{
  "parallel": 4,
  "workerOptions": {
    "resourceLimits": {
      "maxOldGenerationSizeMb": 2048
    }
  }
}

并行时 BeforeAll/AfterAll 默认每个 worker 执行一次;如需只跑一次,用 {on: HookTarget.COORDINATOR}。若需限制某些场景不能并行,可用 setParallelCanAssign(fn) 自定义分配规则,官方还提供了 parallelCanAssignHelpers.atMostOnePicklePerTag(['@tag1', '@tag2']) 之类的便捷助手:

setParallelCanAssign(function (pickleInQuestion, picklesInProgress) {
  // 名字中含 example 的 pickle 同时只能运行一个
  if (pickleInQuestion.name.includes('example')) {
    return picklesInProgress.every((p) => !p.name.includes('example'))
  }
  return true
})

自定义 Formatter 还可以通过 testCaseStarted 信封对象上的 workerId 属性区分消息来自哪个 worker。

重试不稳定场景(Retry)

若某场景偶发失败(比如 10% 概率),可用 Retry 让 Cucumber 多次尝试直到通过或达到上限(详见 docs/retry.md):

  • 配置文件 { retry: 1 }
  • CLI cucumber-js --retry 1

数字表示首次失败后允许的重试次数。要点:

  • 只重试失败的相关场景,而非整个运行;
  • 重试时会用全新的 World 从零执行全部 Hooks 与步骤,失败尝试不保留任何状态;
  • 重试后通过则整体计为通过,但每次尝试的细节都会发出,供 formatter 使用。

配合 retryTagFilter 标签表达式可只针对特定场景重试({ retry: 1, retryTagFilter: '@flaky' } 或 cucumber-js --retry 1 --retry-tag-filter @flaky)。Retry 不建议日常使用,仅作为"无法修复但价值高"的权衡手段。

只重跑失败(Rerun)

TDD 工作流(跑全部 → 修失败 → 只跑失败 → 循环)可以用 rerun formatter 自动化(详见 docs/rerun.md)。先启用:

  • 配置文件 { format: ['rerun:@rerun.txt'] }
  • CLI cucumber-js --format rerun:@rerun.txt

输出文件名必须以 @ 开头(这样 cucumber-js 才能与 feature 文件区分),并记得加入 .gitignore。文件内容形如:

features/adding.feature:3:19
features/editing.feature:8

之后只需 cucumber-js @rerun.txt 即可只跑上次失败的那些场景;每轮通过后失败条目会自动从文件中消失,直到文件清空(指向空 rerun 文件时不会运行任何场景)。默认条目以换行分隔,可用 formatOptions.rerun.separator 修改(注意解析器目前只支持默认分隔符)。

跨机器分片(Sharding)

Sharding(v12.2.0+)把整套测试拆到多个完全独立的测试运行中,适合多台机器或多个 CI Job 并行(区别于单运行内的 Parallel,详见 docs/sharding.md):

  • 配置文件 { shard: '1/3' }
  • CLI cucumber-js --shard 1/3

格式为 INDEX/TOTAL(INDEX 从 1 开始)。例如 90 个场景、3 个分片,则每个分片跑 30 个:1/3 跑第 1、4、7… 个,2/3 跑第 2、5、8… 个,3/3 跑第 3、6、9… 个。常见 CI 用法(GitHub Actions):

jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test -- --shard ${{ matrix.shard }}/3

Sharding 与 Parallel 可以组合:先分片到多台机器,再在每台机器上用 Parallel 用满多核。

快速失败(Fail Fast)

默认 Cucumber 会跑完整个套件并汇报所有失败。failFast 让测试失败即停止,适合"一次只修一个失败"的流程(详见 docs/fail_fast.md):

  • 配置文件 { failFast: true }
  • CLI cucumber-js --fail-fast

与 Retry 配合时:重试耗尽后仍失败才算失败;重试后通过则算通过,所以 failFast 不会阻止 Retry 发生。与 rerun 文件结合可以高效地逐批处理失败。

干跑(Dry Run)

dryRun 模式会完成所有聚合工作(解析 feature、加载支持代码等)但不实际执行测试(详见 docs/dry_run.md):

  • 配置文件 { dryRun: true }
  • CLI cucumber-js --dry-run

具体行为:不执行任何 Hooks;步骤报告为 "skipped";未定义与歧义步骤会被报告但不会导致进程失败。典型用途:用 usage formatter 找出未使用的步骤定义、用 snippets formatter 批量生成未定义步骤的 Snippets、核对路径/标签表达式命中的场景是否符合预期。

扩展能力:插件、自定义 Formatter 与 JavaScript API

插件系统

从 v12.5.0 起,Cucumber 的功能可通过插件扩展(详见 docs/plugins.md)。插件可以监听 Cucumber 的消息流、钩住核心行为做修改、向用户输出额外信息。通过 plugin 配置项指定(可重复):

  • 配置文件 { plugin: ['@my-org/some-plugin', '@my-org/other-plugin'] }
  • CLI cucumber-js --plugin @my-org/some-plugin --plugin @my-org/other-plugin

插件的最简形态是默认导出一个带 type: 'plugin' 与 coordinator 函数的对象:

export default {
  type: 'plugin',
  coordinator: (context) => {
    // do stuff here
  }
}

插件函数接收的 context 包含:operation(正在运行的 Cucumber 操作)、on(event, handler)(注册事件处理器)、transform(event, handler)(注册转换器)、options(用户配置的选项)、logger(输出到 stderr 的迷你日志器)、environment(当前环境属性)。生命周期上,插件在配置解析后不久初始化(可做异步设置),测试运行结束后、Cucumber 退出前停止(返回的 cleanup 函数会在退出前执行):

export default {
  type: 'plugin',
  coordinator: async (context) => {
    await doSomeAsyncSetup()
    return async () => {
      await doSomeAsyncTeardown()
    }
  }
}

可监听的事件有 message(每个 Cucumber Message)、paths:resolve(路径解析完成)、publish:url(发布报告占位成功)。可注册的转换有 pickles:filter 与 pickles:order,用于过滤/排序将要运行的 Pickle。若插件需要专属配置,可用 optionsKey 指定从 pluginOptions 中取哪一块;TypeScript 用户可用 Plugin<MyPluginOptions> 类型获得完整类型检查(见 src/plugin、src/plugin/plugin_manager.ts)。Cucumber 自带的过滤、发布、分片功能本身就是以插件实现的,可作参考:src/filter/filter_plugin.ts、src/publish/publish_plugin.ts、src/sharding/sharding_plugin.ts。

自定义 Formatter

自定义 formatter 是插件的一种精简形态(详见 docs/custom_formatters.md),默认导出 { type: 'formatter', formatter: (context) => {...} }。context 提供 on('message', handler)、options、logger、write(buffer)(写入输出流)与 stream。最小示例——在测试运行结束时输出结果:

export default {
  type: 'formatter',
  formatter: ({
    on,
    write
  }) => {
    on('message', envelope => {
      if (envelope.testRunFinished) {
        write(envelope.testRunFinished.success ? 'Success' : 'Failure')
      }
    })
  }
}

同样支持 optionsKey 取配置块、FormatterPlugin<MyOptions> 类型检查。发布为 npm 包时,确保 formatter 插件对象是 package.json 入口的默认导出,用户即可用包名直接引用。上一代 formatter 架构(基于 Formatter 类)仍受支持但官方推荐切换到新架构与 Cucumber Messages。

JavaScript API:在进程内编程式运行

大多数用户用不到,但要把 Cucumber 集成进更大的框架时,可用其 JavaScript API(入口为 @cucumber/cucumber/api,详见 docs/javascript_api.md)。最小示例:

import { loadConfiguration, runCucumber } from '@cucumber/cucumber/api'

const { runConfiguration } = await loadConfiguration()
const { success } = await runCucumber(runConfiguration)
console.log(success)

核心函数 runCucumber、loadConfiguration、loadSources、loadSupport 都接受可选的 environment 参数(cwd、stdout、stderr、env、debug),缺省时自动从 process 取默认值。配置有两种形态:

  • "用户"配置:扁平格式,用户在配置文件/CLI 中表达的形态(可部分提供,缺省补默认值);
  • "运行"配置:runCucumber 需要的结构化格式,分为 sources、support、formats、runtime 四个块,所有字段必填。推荐用 loadConfiguration 帮你完成"默认值 + 用户配置文件 + 额外值"的合并(合并逻辑见 src/api/load_configuration.ts)。
// 计算测试计划(含过滤与排序后的结果)
import { loadConfiguration, loadSources } from '@cucumber/cucumber/api'

const { runConfiguration } = await loadConfiguration()
const { plan } = await loadSources(runConfiguration.sources)
console.dir(plan)
// 预加载并复用支持代码库(同进程多次运行时必须复用,
// 因为 Node.js 会缓存首次 import 的模块)
import { loadConfiguration, loadSupport, runCucumber } from '@cucumber/cucumber/api'

const { runConfiguration } = await loadConfiguration()
const support = await loadSupport(runConfiguration)
const result1 = await runCucumber({ ...runConfiguration, support })
const result2 = await runCucumber({ ...runConfiguration, support })

runCucumber 的完整执行管线(解析路径 → 编译 Pickle → 初始化 formatter/插件 → 执行 → 汇总结果)可以在 src/api/run_cucumber.ts 中看到。

开发实践:转译、ESM、调试、超时与 Snippets

使用 TypeScript 等语言(转译)

步骤定义可以用需要编译为 JavaScript 的语言编写并即时编译(详见 docs/transpiling.md)。以 TypeScript 为例,建议安装 tsx(无需 tsconfig.json 即可工作):

npm install --save-dev tsx

先确认编译目标模块格式(看 package.json 的 type 字段:module 为 ESM,commonjs 或缺失为 CommonJS)。

  • CommonJS:用 requireModule 注册 tsx/cjs,再用 require 加载 TS 支持代码:
    • 配置文件 { requireModule: ['tsx/cjs'], require: ['features/step-definitions/**/*.ts'] }
    • CLI npx cucumber-js --require-module tsx/cjs --require 'features/step-definitions/**/*.ts'
  • ESM:需要一个显式注册 tsx 的入口文件,再用 import 选项依次加载:
    // tsx-register.js
    import { register } from 'tsx/esm/api'
    register()
    
    • 配置文件 { import: ['./tsx-register.js', 'features/step-definitions/**/*.ts'] }
    • CLI npx cucumber-js --import ./tsx-register.js --import 'features/step-definitions/**/*.ts'

Source maps 能保证报告与堆栈追踪到原始 TS 源码:tsx、ts-node、@babel/register 默认已开启;对"已转译"的共享库代码,需确认产物末尾有 //# sourceMappingURL= 注释,并启用 Node.js 的 --enable-source-maps 标志。

原生 ES Modules

支持代码也可以直接用原生 ESM 语法编写(无需转译,详见 docs/esm.md)。ESM 支持代码需用 import 选项指定(features 目录下的 .mjs 文件会被自动发现导入)。配置文件、自定义 formatter、自定义 snippet 语法也都可以是 ESM,并且可以与 CommonJS 混用(增量迁移)。

调试日志

排查问题时可开启 debug 日志:Cucumber 使用 debug 库的 cucumber scope,设置环境变量即可:

DEBUG=cucumber

启用后 Cucumber 会把配置解析等诊断信息输出到 stderr(详见 docs/debugging.md)。

超时控制

默认异步 Hooks 与步骤的超时是 5000 毫秒(详见 docs/support_files/timeouts.md)。全局修改:

var {setDefaultTimeout} = require('@cucumber/cucumber');

setDefaultTimeout(60 * 1000);

单个 Hook/步骤单独设置:

var {Before, Given} = require('@cucumber/cucumber');

Before({timeout: 60 * 1000}, function() {
  // 慢速的浏览器/文件系统/网络操作
});

Given(/^a slow step$/, {timeout: 60 * 1000}, function() {
  // ...
});

注意 setDefaultTimeout 必须在全局调用(不要在步骤、Hook 或 World 类里调用)。将超时设为 -1 可禁用(官方强烈不建议,除非你自己实现超时保护),必要时可用同样导出给用户的 wrapPromiseWithTimeout 辅助函数(源码在 src/time.ts)。

未定义步骤的 Snippets

BDD 流程中常先写 Feature 再写实现。运行 cucumber-js 遇到未定义步骤时,会为每个未定义步骤生成一段骨架代码(详见 docs/snippets.md),例如:

Then('I remove the todo {string}', function (string) {
  // Write code here that turns the phrase above into concrete actions
  return 'pending';
});

默认生成同步风格。可通过 formatOptions.snippetInterface 切换为支持异步的风格:

  • "async-await":输出 async function,可用 await(不确定时推荐);
  • "callback":输出带最后一个 callback 参数的普通函数;
  • "promise":输出返回 Promise 的普通函数;
  • "synchronous":无异步模式的普通函数。

例:--format-options '{"snippetInterface":"async-await"}' 会生成:

Then('I remove the todo {string}', async function (string) {
  // Write code here that turns the phrase above into concrete actions
  return 'pending';
});

formatOptions.snippetSyntax 还可指定自定义 snippet 语法模块。

附件、数据表与报告发布

  • 附件(Attachments):用 this.attach 向 message/json 等 formatter 输出文本、图片等数据(支持指定 mediaType、fileName,支持 stream.Readable/Buffer,base64 字符串可加 base64: 前缀,如 { mediaType: 'base64:image/png' });this.log 以 text/x.cucumber.log+plain 类型附加日志;this.link 以 text/uri-list 类型附加链接(详见 docs/support_files/attachments.md)。附件也会被 progress、progress-bar、summary 等 formatter 打印(仅 text/plain 可见),适合并行模式下调试。
  • 数据表(Data Tables):带表头的步骤可调用 hashes()(每行转对象,表头为键)、rows()(不含首行的二维数组);无表头时用 raw()(二维数组)、rowsHash()(首列键、次列值的对象)、transpose()(转置后的新实例)。示例见 features/data_tables.feature。
  • 发布报告(Publishing):可用 publish: true(CLI --publish,或环境变量 CUCUMBER_PUBLISH_ENABLED=true)把报告发布到 Cucumber Reports 免费服务,运行结束会打印报告 URL;报告匿名且 24 小时自毁(除非认领)。私有服务可通过 CUCUMBER_PUBLISH_URL、CUCUMBER_PUBLISH_TOKEN 指定,代理场景设置标准代理环境变量并加 NODE_USE_ENV_PROXY=1(需 Node.js 22.21.0/24.0.0+,详见 docs/publishing.md)。

用 Profiles 组合配置

当项目存在多套运行参数(本地 vs CI、不同浏览器/视口)时,Profiles 可以把配置捆绑成命名切片,用 --profile/-p 引用(可重复、可与 CLI 直传选项叠加,CLI 选项追加在 Profile 之后):

cucumber-js -p my_profile

示例——本地默认配置与 CI 配置:

const common = {
  requireModule: ['ts-node/register'],
  require: ['support/**/*.ts'],
  worldParameters: {
    appUrl: process.env.MY_APP_URL || 'http://localhost:3000/'
  }
}

module.exports = {
  default: {
    ...common,
    format: ['progress-bar', 'html:cucumber-report.html'],
  },
  ci: {
    ...common,
    format: ['html:cucumber-report.html'],
    publish: true
  }
}

ESM 下用 export default 表示默认 profile、命名导出表示其他 profile;default 也可以是一个返回 profiles 对象的(异步)函数,用于动态生成配置。由于 Cucumber 不允许自定义 CLI 参数,常见做法是用 profile 作为 --world-parameters 的别名(如 desktop/phone/tablet、chromium/firefox/webkit 各定义一段 JSON),组合调用 cucumber-js -p webkit -p phone 即可,多次提供时参数会合并、后值优先。

进一步阅读与支持

  • 测试
  • 开发工具

【免费下载链接】cucumber-js

Cucumber for JavaScript

项目地址: https://gitcode.com/gh_mirrors/cu/cucumber-js
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值