WebdriverIO Multiremote 多会话测试完全指南:单次执行协调多个浏览器与移动设备
2026/9/16 12:20:44 网站建设 项目流程

WebdriverIO Multiremote 多会话测试完全指南:单次执行协调多个浏览器与移动设备

【免费下载链接】webdriverioNext-gen browser and mobile automation test framework for Node.js项目地址: https://gitcode.com/GitHub_Trending/we/webdriverio

本文围绕 WebdriverIO 官方文档《Multiremote》展开,系统讲解如何在一个测试中同时创建并协调多个浏览器/移动设备会话:从 standalone 模式下的multiremote()调用、WDIO Testrunner 中 capabilities 对象的配置,到getInstance()select()等实例访问方式、TypeScript 类型扩展,以及底层命令分发与并行执行原理。读完本文,你将掌握编写聊天应用、WebRTC 等多用户协同场景集成测试的完整实战方案。

Multiremote 是什么:单测试内的多会话协调

WebdriverIO 允许你在一个测试中运行多个自动化会话,这一能力被称作Multiremote。它面向的是那些需要"多个用户"同时参与的特性测试——最典型的场景是聊天应用(一个用户发消息、另一个用户收消息并断言)或WebRTC类实时通信应用。

在没有 Multiremote 之前,开发者需要手动创建多个 remote 实例,并逐个在实例上重复执行newSessionurl这类公共命令。Multiremote 则允许你创建一个多路远程实例,然后对所有浏览器"同时"下达命令。正如 packages/webdriverio/src/index.ts 中multiremote()的 JSDoc 所言:

"Instead of creating a couple of remote instances where you need to execute common commands like newSession() or url() on each instance, you can simply create a multiremote instance and control all browsers at the same time."

重要边界:不是并行测试框架

必须先厘清一个容易误用的点——官方文档在 Multiremote 页顶部就给出提示(:::info块):

Multiremote不是用来并行执行你所有测试的。它旨在帮助协调多个浏览器和/或移动设备,以完成特殊的集成测试(例如聊天应用)。

也就是说,Multiremote 的定位是"同一个测试内部的跨会话协同",而不是把不同 spec 分发到不同浏览器上并行跑(那是 WDIO Testrunner 常规的并行能力)。从 packages/wdio-config/src/lib/config.ts 相关的配置结构(详见 Capabilities.ts 的TestrunnerCapabilities类型)可以看到,RequestedMultiremoteCapabilities与普通 capabilities 数组是两种并列形态。

返回值约定:按声明顺序返回结果数组

所有 multiremote 实例执行命令后返回的都是数组:第一个结果对应 capabilities 对象中第一个定义的 capability,第二个对应第二个,以此类推。这个约定从文档到源码、测试都保持一致——例如 packages/webdriverio/tests/multiremote.test.ts 中执行browser.execute(() => 'foobar')断言返回['foobar', 'foobar']

使用 standalone 模式创建 Multiremote 实例

在 standalone(独立脚本)模式下,核心 API 就是从webdriverio包导入的multiremote()函数。它接收一个以实例名为 key、capabilities 为 value 的对象

import { multiremote } from 'webdriverio' (async () => { const browser = await multiremote({ myChromeBrowser: { capabilities: { browserName: 'chrome' } }, myFirefoxBrowser: { capabilities: { browserName: 'firefox' } } }) // 两个浏览器同时打开 URL await browser.url('http://json.org') // 同时调用命令,返回结果数组 const title = await browser.getTitle() expect(title).toEqual(['JSON', 'JSON']) // 同时在两个浏览器上定位并点击元素 const elem = await browser.$('#someElem') await elem.click() // 只让某一个浏览器(Firefox)点击 await elem.getInstance('myFirefoxBrowser').click() })()

这段代码体现了 Multiremote 的三个关键用法:

  1. 并行命令browser.url()browser.getTitle()一次调用同时作用于所有实例;
  2. 元素级并行:通过browser.$()得到的 multiremote 元素,其click()等命令默认在所有实例上执行;
  3. 单实例操作:通过elem.getInstance('myFirefoxBrowser')精确指定某个实例单独执行命令。

底层实现:实例创建与会话封装

从源码看,multiremote()的实际执行路径非常清晰(packages/webdriverio/src/index.ts):

const multibrowser = new MultiRemote() const browserNames = Object.keys(params) await Promise.all( browserNames.map(async (browserName) => { const instance = await remote(params[browserName]) return multibrowser.addInstance(browserName, instance) }) )

并行地为每个命名 capability 调用remote()创建独立会话,再通过addInstance()注册到MultiRemote实例的instances字典中(packages/webdriverio/src/multiremote.ts)。随后用ProtocolDriver.attachToSession()把一个"空会话"包装在所有实例之上,形成统一的多路远程浏览器对象。

命令如何分发到每个实例

当你在多路浏览器对象上调用任何命令时,最终都会进入MultiRemote.commandWrapper()(packages/webdriverio/src/multiremote.ts):

const result = await Promise.all( scopeEntries.map( ([, instance]) => instancecommandName ) )

命令通过Promise.all所有实例上同时执行;对于$命令,会通过MultiRemote.elementWrapper()把各实例返回的元素包装成多路元素对象(packages/webdriverio/src/multiremote.ts),这样后续click()getText()等元素级命令也能继续自动广播到每个实例。而元素命令的自动重试、stale element 处理等则由 middlewares.ts 中的multiremoteHandler承接——它对元素调用同样遍历this.instances并用Promise.all汇总结果。

使用 WDIO Testrunner 配置 Multiremote

在 Testrunner 模式下,你不需要手动调用multiremote()——只需把wdio.conf.js里的capabilities从"数组"改为"以浏览器名称为 key 的对象":

export const config = { // ... capabilities: { myChromeBrowser: { capabilities: { browserName: 'chrome' } }, myFirefoxBrowser: { capabilities: { browserName: 'firefox' } } } // ... }

这样 WebdriverIO 会自动创建两个 WebDriver 会话(Chrome + Firefox)。而且远不止浏览器:借助 Appium 你还可以同时启动两台移动设备,或者"一台移动设备 + 一个浏览器"的混合组合。

在数组内并行运行多组 Multiremote

如果你希望多组多路会话并行执行,可以把 capabilities 对象放进一个数组——每个数组元素代表一组 multiremote 会话。关键约束是:每组内部的每个浏览器都必须包含capabilities字段,WebdriverIO 正是依据这一字段来区分"普通多能力数组"与"多路远程组":

export const config = { // ... capabilities: [{ myChromeBrowser0: { capabilities: { browserName: 'chrome' } }, myFirefoxBrowser0: { capabilities: { browserName: 'firefox' } } }, { myChromeBrowser1: { capabilities: { browserName: 'chrome' } }, myFirefoxBrowser1: { capabilities: { browserName: 'firefox' } } }] // ... }

此配置会并行创建两组会话(组 0:Chrome+Firefox;组 1:Chrome+Firefox)。这一点与仓库内 e2e 配置的形态一致——e2e/wdio/wdio-multiRemote.conf.ts 中capabilities就是包含browserAbrowserBbrowserC三个实例的对象数组,每个实例各自声明了 Chrome(含goog:chromeOptionsheadless 参数)与 Firefox(含moz:firefoxOptions)的浏览器级 options。

混搭云端服务与本地后端

Multiremote 甚至可以同时使用不同的后端:例如一个本地 Chrome + 一个云端浏览器,或者本地 WebDriver/Appium + Selenium Standalone。WebdriverIO 会自动检测 capabilities 中是否包含特定云端选项:

  • bstack:options(BrowserStack)
  • sauce:options(SauceLabs)
  • tb:options(TestingBot)

只要检测到这些字段,就自动路由到对应云服务。下面是一个"本地 Chrome + BrowserStack Firefox"的混搭配置:

export const config = { // ... user: process.env.BROWSERSTACK_USERNAME, key: process.env.BROWSERSTACK_ACCESS_KEY, capabilities: { myChromeBrowser: { capabilities: { browserName: 'chrome' } }, myBrowserStackFirefoxBrowser: { capabilities: { browserName: 'firefox', 'bstack:options': { // ... } } } }, services: [ ['browserstack', 'selenium-standalone'] ], // ... }

任意 OS/浏览器的组合都是可行的(移动端、桌面端皆可)。你在测试中通过browser变量调用的所有命令都会在每个实例上并行执行,从而简化集成测试的编写并加速执行。

命令执行的同步语义与结果汇总

所有 multiremote 命令默认是逐个实例按顺序同步推进的:每个命令都会等所有浏览器都执行完毕后才返回。官方文档特别指出这一点——"each command is executed one by one. This means that the command finishes once all browsers have executed it." 这种语义的好处是:各个浏览器始终保持动作同步,你始终清楚当前发生了什么,不会出现一个浏览器已执行完、另一个还在半途的竞态混乱。

调用 URL 后,每个命令的返回结果都是"以浏览器名为 key、命令结果为 value"的对象形式(对元素命令则是数组形式,按声明顺序排列)。WDIO Testrunner 中的实例如下:

// wdio testrunner 示例 await browser.url('https://www.whatismybrowser.com') const elem = await $('.string-major') const result = await elem.getText() console.log(result[0]) // 返回: 'Chrome 40 on Mac OS X (Yosemite)' console.log(result[1]) // 返回: 'Firefox 35 on Mac OS X (Yosemite)'

这一行为在仓库测试中被反复验证——例如 packages/webdriverio/tests/multiremote.test.ts 对browser.$('#foo')得到的元素调用getSize(),断言返回[{ width: 50, height: 30 }, { width: 50, height: 30 }];e2e/wdio/headless/multiRemoteTest.e2e.ts 中的多实例 e2e 测试也断言multiRemoteBrowser.getTitle()返回三个相同的标题数组。

让每个浏览器各做各的:getInstance 与全局实例

虽然同步并行很方便,但有些场景必须让各浏览器做不同的事。比如测试聊天应用时:一个浏览器负责发送消息,另一个浏览器等待接收消息再断言。

使用 WDIO Testrunner 时,WebdriverIO 会把各浏览器实例注册到全局作用域,你在测试中可以通过全局变量(如myChromeBrowser)或browser.getInstance('myChromeBrowser')获取单个实例:

const myChromeBrowser = browser.getInstance('myChromeBrowser') await myChromeBrowser.$('#message').setValue('Hi, I am Chrome') await myChromeBrowser.$('#send').click() // 等待消息到达 await $('.messages').waitForExist() // 检查某条消息中是否包含 Chrome 发出的内容 assert.true( ( await $$('.messages').map((m) => m.getText()) ).includes('Hi, I am Chrome') )

在这个示例中,当myChromeBrowser点击#send之后,myFirefoxBrowserwaitForExist才开始等待新消息出现——这正是"一个发送、一个接收"的协同测试范式。从 packages/wdio-runner/src/index.ts 可见,runner 会把浏览器对象以multiremotebrowser为名注入全局作用域以提供更好的类型支持;而实例的获取在 multiremote.ts 中就是一个简单的字典查找:

propertiesObject.getInstance = { value: (browserName: string) => this.instances[browserName] }

在 standalone 模式下,则通过elem.getInstance('myFirefoxBrowser')或在modifier()中把每个实例直接挂载到客户端对象上(client[identifier] = instance,见 multiremote.ts),因此你甚至可以直接写browser.myChromeBrowser

用字符串经 browser 对象访问实例

除了全局变量,你还可以通过browser对象本身以字符串下标访问实例,例如browser["myChromeBrowser"]browser["myFirefoxBrowser"];用browser.instances可以拿到所有实例名的列表。这在编写"任一浏览器都能复用"的测试步骤时尤其有用。

典型的落地组合是Cucumber + 数据驱动的用户角色。假设你的 capability 定义为:

// wdio.conf.js capabilities: { userA: { capabilities: { browserName: 'chrome' } }, userB: { capabilities: { browserName: 'chrome' } } }

对应的.feature文件与 step 定义如下:

When User A types a message into the chat
When(/^User (.) types a message into the chat/, async (userId) => { await browser.getInstance(`user${userId}`).$('#message').setValue('Hi, I am Chrome') await browser.getInstance(`user${userId}`).$('#send').click() })

这样User AUser B等角色就会被映射到对应的userAuserB实例,彻底避免了在 step 里写死浏览器名的重复代码。

用 select() 缩小多路范围

仓库源码中还提供了select()方法(需设置环境变量WDIO_ENABLE_MULTI_REMOTE_SELECT=true启用),它可以从多路对象中筛选出部分实例继续链式调用,例如只对browserA查询元素。其底层实现在 multiremote.ts:通过实例名列表构建新的MultiRemote并复用原属性描述符,若所有请求的实例名都无效则抛出None of the following requested instances are valid: ...。相关的select行为(含元素级select、链式select、筛选后自定义命令的保留等)在 multiRemoteTest.e2e.ts 中有大量 e2e 断言覆盖。

使用 TypeScript 扩展 Multiremote 类型

如果你使用 TypeScript,并且希望直接从多路远程对象上点出各实例,可以扩展 multiremote 的类型定义。假设你的配置如下(注意MultiremoteConfig类型):

// wdio.conf.ts export const config: WebdriverIO.MultiremoteConfig = { // ... capabilities: { myAppiumDriver: { // ... }, myChromeDriver: { // ... } } // ... }

然后在一个全局.d.ts文件中扩展MultiRemoteBrowser接口,声明你自己的驱动实例名:

// wdio.d.ts declare namespace WebdriverIO { interface MultiRemoteBrowser { myAppiumDriver: WebdriverIO.Browser myChromeDriver: WebdriverIO.Browser } }

之后即可在测试中获得完整的类型提示与直接访问能力:

multiRemoteBrowser.myAppiumDriver.$$(...) multiRemoteBrowser.myChromeDriver.$(...)

这一机制背后的类型基础在 packages/webdriverio/src/types.ts 中定义:MultiRemoteBrowser继承自MultiRemoteBase,后者通过[key: string]: WebdriverIO.Browser之类的索引签名允许动态实例名(具体见 types.ts 中MultiRemoteBaseMultiRemoteBrowserTypeMultiRemoteElement等接口)。扩展声明就相当于把索引签名里的动态名"收紧"成你项目里真实存在的实例,从而获得精确的类型推导。

源码级全景:Multiremote 的完整调用链

综合以上内容,一条多路命令的完整生命周期可以归纳如下:

  1. 创建multiremote(params)(或 Testrunner 根据 capabilities 对象)实例化MultiRemote,并行对每个命名 capability 调用remote()创建子会话并addInstance()(index.ts);
  2. 包装:用ProtocolDriver.attachToSession()把空会话modifier包装到所有实例之上,得到统一的多路对象;modifier为每个命令名绑定commandWrapper,并提供getInstanceselect等专属方法(multiremote.ts);
  3. 分发:调用命令时commandWrapperPromise.all让每个实例各自执行同名命令,返回结果数组(multiremote.ts);
  4. 元素包装$/$$的结果通过MultiRemote.elementWrapper()变成多路元素对象,元素上的后续命令同样广播到各实例;元素命令的容错(隐式等待、stale 元素重取、不可交互重试)由 middlewares.ts 的elementErrorHandler配合multiremoteHandler完成;
  5. 事件与自定义命令MultiRemoteDriveron/once/emit等事件 API 广播到每个子实例(multiremote.ts);addCommandoverwriteCommand也会被转发到每个子实例并保留在 multiremote 原型上(index.ts)。

此外,仓库的 e2e 测试还展示了真实可跑的 headless 多路配置:在 e2e/wdio/wdio-multiRemote.conf.ts 中,browserA为 headless Chrome、browserB为 headless Firefox、browserC为 headless Chromium(Linux 下额外追加no-sandbox参数以规避容器内会话创建失败),并通过WebdriverIO.MultiremoteConfig类型标注;对应测试 multiRemoteTest.e2e.ts 覆盖了标题并行断言、元素链式查询、select()筛选、共享存储服务(sharedStore)、自定义命令保留等场景。这套配置可以作为你自己搭建多路测试的现成蓝本。

小结与适用边界

Multiremote 是 WebdriverIO 面向"多用户协同集成测试"提供的开箱即用方案:它用一个统一对象驱动多个会话并行执行命令、以结果数组按声明顺序返回,并提供getInstance/ 全局变量 / 字符串下标 /select()多种方式定位单个实例,还能无缝混搭本地与云端后端、浏览器与移动设备。使用时请务必记住它的定位边界——它是会话内的协调工具,而非测试用例的并行分发器;如果你的目标是加速大批量 spec 的执行,应该使用 Testrunner 原生的 capabilities 数组并行机制,而不是 Multiremote。

【免费下载链接】webdriverioNext-gen browser and mobile automation test framework for Node.js项目地址: https://gitcode.com/GitHub_Trending/we/webdriverio

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询