WebdriverIO Testrunner 实战示例全解:Mocha、Jasmine、Cucumber、Multiremote 与自定义扩展

原创2026-09-15 11:31:161,710 阅读
文章标签:测试质量保障

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 分钟
    }
}

两个差异点值得注意:

  1. specs 使用 *.spec.ts 通配符,一次匹配目录下全部 Jasmine 规格文件;
  2. 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 工程化最直接的一条路径。

登录后查看全文
webdriverio