WebdriverIO Testrunner 实战示例全解:Mocha、Jasmine、Cucumber、Multiremote 与自定义扩展
WebdriverIO Testrunner 实战示例全解:Mocha、Jasmine、Cucumber、Multiremote 与自定义扩展
WebdriverIO 官方的 examples/wdio 目录是一套开箱即用的 Testrunner 示例工程,覆盖了 Mocha、Jasmine、Cucumber 三种测试框架,以及 Multiremote 多浏览器会话、自定义 Reporter、自定义 Service 和基于 Vite + Vue 的组件测试。本文以 examples/wdio/README.md 为主线,结合仓库中各示例的配置文件与测试源码,讲解每个示例的目录结构、运行方式、配置要点与底层扩展机制,读完后你可以直接把这套示例作为自己项目工程化的起点。
示例工程一览
examples/wdio 是一个独立的 npm 包("name": "wdio-example",见 examples/wdio/package.json),它采用 pnpm workspace 的 workspace:* 方式引用仓库内所有 WebdriverIO 相关包,包括 @wdio/cli、@wdio/config、@wdio/local-runner、各框架适配器(@wdio/mocha-framework、@wdio/jasmine-framework、@wdio/cucumber-framework)、@wdio/globals、webdriverio、webdriver,以及 @wdio/browser-runner(用于组件测试)。
整个示例集合围绕以下子目录展开:
| 子目录 | 演示主题 | 测试框架 |
|---|---|---|
mocha/ |
最基础的 Testrunner 用法 | Mocha |
jasmine/ |
Jasmine 框架适配 | Jasmine |
cucumber/ |
BDD 特性文件与步骤定义 | Cucumber |
multiremote/ |
多浏览器并行会话 | Mocha |
custom-reporter/ |
嵌入自定义 Reporter | Mocha |
custom-service/ |
自定义 Service 生命周期钩子 | Mocha |
vite-vue-example/ |
基于浏览器运行时的组件测试 | Mocha(browser-runner) |
快速运行:一条命令跑一套测试
原文档列出了每个示例对应的 npm script(见 examples/wdio/README.md),这些脚本在 examples/wdio/package.json 的 scripts 字段中均有真实定义:
# mocha 测试
$ npm run test:mocha
# jasmine 测试
$ npm run test:jasmine
# cucumber 测试
$ npm run test:cucumber
# multiremote 多浏览器测试套件
$ npm run test:multiremote
# 带自定义 reporter 的测试套件
$ npm run test:customReporter
# 自定义 service 的测试套件
$ npm run test:customService
# 组件测试(Vite + Vue)
$ npm run test:viteVue
由于本仓库使用 pnpm workspace 管理,你也可以直接用 pnpm run test:mocha 等命令执行。每个脚本底层都通过 wdio run <配置文件路径> 启动 Testrunner,例如:
"test:mocha": "wdio run ./mocha/wdio.conf.ts",
"test:multiremote": "wdio run ./multiremote/wdio.conf.js",
"test:viteVue": "cd ./vite-vue-example && npm run wdio"
注意 test:multiremote 和 test:customReporter、test:customService 使用 JavaScript 配置文件(.js),而 Mocha、Jasmine、Cucumber 示例则直接使用 TypeScript 配置文件(.ts),这印证了 WebdriverIO 原生支持 TS 配置无需额外编译步骤。
前提:运行示例前需要在仓库根目录执行依赖安装(pnpm workspace 会自动链接本地包);浏览器方面示例统一使用
browserName: 'chrome',需要本机具备可用的 Chrome 与对应驱动。
Mocha 示例:最小可运行的 Testrunner 工程
Mocha 示例由 examples/wdio/mocha/wdio.conf.ts 与 examples/wdio/mocha/mocha.test.ts 组成,是最小但完整的配置模板。
配置文件核心字段:
export const config: WebdriverIO.Config = {
specs: ['./mocha.test.ts'], // 指定测试文件
capabilities: [{ browserName: 'chrome' }], // 浏览器能力
logLevel: 'trace', // 日志级别
framework: 'mocha', // 测试框架
outputDir: path.resolve(__dirname, 'logs'), // 输出目录
reporters: ['spec', 'dot', 'junit'], // 报告器
mochaOpts: { ui: 'bdd', timeout: 15000 }, // Mocha 选项
onPrepare: function() { console.log('let\'s go') },
onComplete: function() { console.log('that\'s it') }
}
要点说明:
specs可以是数组,也可以像 Jasmine/Cucumber 示例那样嵌套一层数组(例如specs: [[path.resolve(...)]]),后者表示同一批文件在多个 worker 中重复执行,常用于并发冒烟验证;本示例用绝对路径解析__dirname,避免工作目录不同导致相对路径失效。reporters同时启用了spec、dot、junit三个内置 Reporter,其中junit会生成 JUnit XML 报告供 CI 解析,说明报告器支持多实例叠加。mochaOpts.ui指定bdd风格,timeout设置单用例超时为 15 秒。onPrepare/onComplete是全局生命周期钩子,分别在全部 worker 启动前和整体完成后触发。
对应的测试文件演示了 @wdio/globals 的用法:
import { browser, expect } from '@wdio/globals'
describe('webdriver.io page', () => {
it('should be a pending test') // 悬空用例,仅占位不执行
it('should have the right title', async () => {
await browser.url('https://webdriver.io')
await expect(browser).toHaveTitle('WebdriverIO · Next-gen browser and mobile automation test framework for Node.js | WebdriverIO')
})
})
从源码结构看,browser 与 expect 均来自 @wdio/globals(packages/wdio-globals/src/index.ts),它把全局实例(browser、$、$$、expect 等)封装为可直接 import 的模块,避免依赖隐式全局变量;toHaveTitle 是 expect-webdriverio 提供的断言(见 packages/expect-webdriverio),会对元素/浏览器状态做自动等待后断言。
Jasmine 示例:同样的模式,不同的框架
examples/wdio/jasmine/wdio.conf.ts 与 Mocha 示例结构几乎一致,只需切换 framework: 'jasmine' 并提供 jasmineOpts:
export const config: WebdriverIO.Config = {
specs: [[path.resolve(__dirname, '*.spec.ts')]], // 通配匹配所有 .spec.ts
capabilities: [{ browserName: 'chrome' }],
logLevel: 'trace',
outputDir: __dirname,
framework: 'jasmine',
reporters: ['spec'],
jasmineOpts: {
defaultTimeoutInterval: 1000 * 60 * 3 // 3 分钟
}
}
两个差异点值得注意:
specs使用*.spec.ts通配符,一次匹配目录下全部 Jasmine 规格文件;- Jasmine 用
jasmineOpts.defaultTimeoutInterval控制用例超时(示例设为 3 分钟),与 Mocha 的mochaOpts.timeout对应。
examples/wdio/jasmine/jasmine.spec.ts 中的用例写法与 Mocha 示例几乎相同(describe/it),说明 WebdriverIO 在框架之上提供了统一的 BDD 风格 API——切换框架时测试代码基本无需改动,只需改配置与适配器包。
Cucumber 示例:Feature 文件 + 步骤定义
Cucumber 示例分为三部分:特性文件、步骤定义与配置。
特性文件
examples/wdio/cucumber/features/my-feature.feature 使用 Gherkin 语法描述行为,演示了 Scenario、Rule、数据表格(Data Tables)等用法:
Feature: Example feature
As a user of WebdriverIO
I should be able to use different commands
to get informations about elements on the page
Scenario: Get size of an element
Given I go on the website "https://github.com/"
Then should the element ".header-logged-out a" be 32px wide and 34.5px high
Rule: Business rule 1
Scenario: Get title of website
Given I go on the website "https://github.com/"
Then should the title of the page be "GitHub: Let’s build from here · GitHub"
Rule: Business rule 2
Scenario: Data Tables
Given I go on the website "http://todomvc.com/examples/react/dist/"
When I add the following groceries
| Item | Amount |
| Milk | 2 |
| Butter | 1 |
| Noodles | 1 |
| Schocolate | 3 |
Then I should have a list of 4 items
步骤定义
examples/wdio/cucumber/step-definitions.ts 通过 @wdio/cucumber-framework 导出的 Given/When/Then 装饰器绑定正则表达式与实现:
import { Given, When, Then } from '@wdio/cucumber-framework'
import { Key } from 'webdriverio'
Given(/^I go on the website "([^"]*)"$/, async (url) => {
await browser.url(url)
})
When(/^I add the following groceries$/, async (table) => {
const newTodo = await $('.new-todo')
table.rawTable.shift()
for (const [item, amount] of table.rawTable) {
await newTodo.addValue(`${item} (${amount}x)`)
await browser.keys(Key.Enter)
await browser.pause(100) // for demo purposes
}
})
Then(/^should the element "([^"]*)" be (\d+(?:\.\d+)?)px wide and (\d+(?:\.\d+)?)px high$/, async (selector, width, height) => {
const elemSize = await $(selector).getSize()
expect(elemSize.width).toBe(Number(width))
expect(elemSize.height).toBe(Number(height))
})
Then(/^should the title of the page be "([^"]*)"$/, async (expectedTitle) => {
await expect(browser).toHaveTitle(expectedTitle)
})
Then(/^I should have a list of (\d+) items$/, async (items) => {
await expect($$('.todo-list li')).toBeElementsArrayOfSize(items)
})
实现细节亮点:
- 元素尺寸步骤用
$(selector).getSize()获取宽高,并通过expect(elemSize.width).toBe(...)逐项断言; - 数据表格步骤解析
table.rawTable(二维数组),跳过头行后逐行向输入框填入商品 (数量x)并按 Enter 提交; - 列表项数量断言使用
toBeElementsArrayOfSize,该断言会等待元素集合达到指定数量; Key.Enter来自webdriverio导出的按键枚举(packages/webdriverio),替代硬编码的 ASCII 码。
配置
examples/wdio/cucumber/wdio.conf.ts 的差异点在于 framework: 'cucumber' 与 cucumberOpts:
export const config: WebdriverIO.Config = {
specs: [[path.resolve(__dirname, 'features', 'my-feature.feature')]],
capabilities: [{ browserName: 'chrome' }],
logLevel: 'error',
framework: 'cucumber',
reporters: ['spec'],
cucumberOpts: {
require: [path.resolve(__dirname, 'step-definitions.ts')] // 加载步骤定义
}
}
cucumberOpts.require 指定步骤定义文件的加载入口,WebdriverIO 会通过自身的 TS 编译管线直接加载 .ts 步骤定义。注意该示例 logLevel 设为 error,仅输出错误级别日志,更适合在特性文件较多的工程中减少噪音。
Multiremote 示例:一次运行多个浏览器
examples/wdio/multiremote/wdio.conf.js 演示了 Multiremote 模式——用一份配置同时管理多个浏览器实例:
exports.config = {
specs: [[path.resolve(__dirname, '*.test.js')]],
capabilities: {
browserA: { capabilities: { browserName: 'chrome' } },
browserB: { capabilities: { browserName: 'chrome' } }
},
logLevel: 'trace',
outputDir: __dirname,
framework: 'mocha',
reporters: ['spec'],
mochaOpts: { ui: 'bdd', timeout: 1000 * 60 * 3 }
}
与普通模式的关键区别:capabilities 不再是数组,而是一个以实例名为键的对象(browserA/browserB),每个键对应一个独立的浏览器会话。测试代码中 browser 是一个代理对象,browser.browserA.url(...)、browser.browserB.url(...) 可以分别操作不同浏览器,从而实现跨浏览器一致性校验或竞态场景复现(见 examples/wdio/multiremote/mocha.test.js)。
自定义 Reporter:继承 WDIOReporter 覆盖事件钩子
examples/wdio/custom-reporter/my.custom.reporter.js 展示了如何编写一个自定义 Reporter:
import WDIOReporter from '@wdio/reporter'
export default class CustomReporter extends WDIOReporter {
constructor(options) {
super(options)
console.log('initialized custom reporter with the following reporter options:', options)
this.write('Some log line')
}
onRunnerStart() { console.log('onRunnerStart') }
onBeforeCommand() { console.log('onBeforeCommand') }
onAfterCommand() { console.log('onAfterCommand') }
onSuiteStart() { console.log('onSuiteStart') }
onHookStart() { console.log('onHookStart') }
onHookEnd() { console.log('onHookEnd') }
onTestStart() { console.log('onTestStart') }
onTestPass() { console.log('onTestPass') }
onTestFail() { console.log('onTestFail') }
onTestRetry() { console.log('onTestRetry') }
onTestSkip() { console.log('onTestSkip') }
onTestEnd() { console.log('onTestEnd') }
onSuiteEnd() { console.log('onSuiteEnd') }
onRunnerEnd() { console.log('onRunnerEnd') }
}
要点:
- Reporter 类继承
@wdio/reporter的WDIOReporter基类(packages/wdio-reporter),this.write()用于向报告流写入内容; - 构造函数接收
options,即配置文件里传入的 reporter 选项; - 通过实现
onRunnerStart、onTestPass、onTestFail、onTestRetry等钩子方法,可以精确感知从 Runner 启动到命令执行、套件、用例、重试、结束的全生命周期。
在 examples/wdio/custom-reporter/wdio.conf.js 中注册 Reporter 的语法是"类 + 选项"的二元组:
import CustomReporter from './my.custom.reporter.js'
// ...
reporters: [[CustomReporter, {
someOption: 'foobar'
}]]
这等价于内置 Reporter 的字符串简写,同时允许向构造函数注入自定义配置对象。
自定义 Service:覆盖完整的生命周期钩子
examples/wdio/custom-service/my.custom.service.js 是自定义 Service 的完整模板,它不像 Reporter 那样继承基类,而是实现一组约定好的生命周期方法:
export default class CustomService {
constructor (config) {
console.log('custom service options:', config.someOption)
}
onPrepare() { console.log('execute onPrepare(config, capabilities)') }
onWorkerStart() { console.log('execute onWorkerStart(cid, caps, specs, args, execArgv)') }
onWorkerEnd() { console.log('execute onWorkerEnd(cid, exitCode, specs, retries)') }
beforeSession() { console.log('execute beforeSession(config, capabilities, specs, cid)') }
before() { console.log('execute before(capabilities, specs)') }
beforeSuite() { console.log('execute beforeSuite(suite)') }
beforeHook() { console.log('execute beforeHook(test, context, hookName)') }
afterHook() { console.log('execute afterHook(test, context, { error, result, duration, passed, retries }, hookName)') }
beforeTest() { console.log('execute beforeTest(test, context)') }
beforeCommand() { console.log('execute beforeCommand(commandName, args)') }
afterCommand() { console.log('execute afterCommand(commandName, args, result, error)') }
afterTest() { console.log('execute afterTest(test, context, { error, result, duration, passed, retries })') }
afterSuite() { console.log('execute afterSuite(suite)') }
after() { console.log('execute after(result, capabilities, specs)') }
afterSession() { console.log('execute afterSession(config, capabilities, specs)') }
onComplete() { console.log('execute onComplete(exitCode, config, capabilities, results)') }
onReload() { console.log('execute onReloadedSessionId, newSessionId)') }
beforeFeature() { console.log('execute beforeFeature(uri, feature, scenarios)') }
beforeScenario() { console.log('execute beforeScenario(uri, feature, scenario, sourceLocation, context)') }
beforeStep() { console.log('execute beforeStep({ uri, feature, step }, context)') }
afterStep() { console.log('execute afterStep({ uri, feature, step }, context, { error, result, duration, passed, retries })') }
afterScenario() { console.log('execute afterScenario(uri, feature, scenario, result, sourceLocation, context)') }
afterFeature() { console.log('execute afterFeature(uri, feature, scenarios)') }
}
这些钩子按执行阶段分为四组:
- 全局阶段:
onPrepare→onWorkerStart(每个 worker 启动)→onWorkerEnd→onComplete; - 会话阶段:
beforeSession→before→ ... →after→afterSession; - 用例级:
beforeSuite/afterSuite、beforeTest/afterTest、beforeHook/afterHook,以及更细粒度的beforeCommand/afterCommand(每次 WebDriver 命令前后); - Cucumber 专用:
beforeFeature/afterFeature、beforeScenario/afterScenario、beforeStep/afterStep,仅在 Cucumber 框架下触发。
在 examples/wdio/custom-service/wdio.conf.js 中,Service 以 services: [[CustomService, { someOption: 'foobar' }]] 的二元组形式注册,构造函数拿到 config 后即可读取 someOption。这是 WebdriverIO 众多官方 Service(如 @wdio/shared-store-service、@wdio/lighthouse-service)的同一套扩展模型,参照此模板即可实现截图、网络监控、跨 worker 数据共享等自定义能力。
组件测试示例:Vite + Vue 与 browser-runner
test:viteVue 脚本进入 examples/wdio/vite-vue-example 独立子工程执行。该示例使用 @wdio/browser-runner(packages/wdio-browser-runner)在真实浏览器中直接运行组件测试,无需额外起 dev server——WebdriverIO 会借助 Vite 完成源码编译与依赖预构建。
示例工程包含标准的 Vite 项目骨架(vite.config.ts、index.html、src/main.ts、src/App.vue 等),组件测试写在 examples/wdio/vite-vue-example/src/components/HelloWorld.test.ts 中,与被测组件 HelloWorld.vue 同目录存放;测试运行配置见 examples/wdio/vite-vue-example/wdio.conf.ts 与 examples/wdio/vite-vue-example/package.json。该子目录还自带独立的 README(examples/wdio/vite-vue-example/README.md)说明更细的运行细节。这种"测试文件与组件同目录"的组织方式,也是组件测试(Component Testing)的推荐实践。
从示例到源码:理解扩展点背后的实现
上述示例之所以能"换框架不改测试代码",底层依赖 WebdriverIO 的分层架构:
- CLI 启动:
wdio run由@wdio/cli解析并调用@wdio/local-runner(packages/wdio-local-runner)在 worker 进程中执行测试; - 框架适配:
@wdio/mocha-framework、@wdio/jasmine-framework、@wdio/cucumber-framework把各框架的钩子翻译成 WebdriverIO 的统一事件流,这正是 Reporter 与 Service 能无差别接收onTestPass、beforeStep等事件的原因; - 全局 API:
@wdio/globals(packages/wdio-globals)与 expect-webdriverio 提供与框架无关的browser、$、$$、expect接口; - Runner 与报告:Runner 负责 worker 调度与结果聚合(packages/wdio-runner),Reporter 与 Service 分别在 packages/wdio-reporter 和 packages/wdio-utils 的支撑下被实例化并订阅生命周期。
因此,无论你后续接入哪种 CI、哪个云测平台,还是编写自己的测试插件,都可以先把 examples/wdio/README.md 中列出的这七类示例跑通,再对照各 packages/* 源码理解其事件流转——这是学习 WebdriverIO 工程化最直接的一条路径。