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 实例,并逐个在实例上重复执行newSession、url这类公共命令。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 的三个关键用法:
- 并行命令:
browser.url()、browser.getTitle()一次调用同时作用于所有实例; - 元素级并行:通过
browser.$()得到的 multiremote 元素,其click()等命令默认在所有实例上执行; - 单实例操作:通过
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就是包含browserA、browserB、browserC三个实例的对象数组,每个实例各自声明了 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之后,myFirefoxBrowser的waitForExist才开始等待新消息出现——这正是"一个发送、一个接收"的协同测试范式。从 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 chatWhen(/^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 A、User B等角色就会被映射到对应的userA、userB实例,彻底避免了在 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 中MultiRemoteBase、MultiRemoteBrowserType、MultiRemoteElement等接口)。扩展声明就相当于把索引签名里的动态名"收紧"成你项目里真实存在的实例,从而获得精确的类型推导。
源码级全景:Multiremote 的完整调用链
综合以上内容,一条多路命令的完整生命周期可以归纳如下:
- 创建:
multiremote(params)(或 Testrunner 根据 capabilities 对象)实例化MultiRemote,并行对每个命名 capability 调用remote()创建子会话并addInstance()(index.ts); - 包装:用
ProtocolDriver.attachToSession()把空会话modifier包装到所有实例之上,得到统一的多路对象;modifier为每个命令名绑定commandWrapper,并提供getInstance、select等专属方法(multiremote.ts); - 分发:调用命令时
commandWrapper用Promise.all让每个实例各自执行同名命令,返回结果数组(multiremote.ts); - 元素包装:
$/$$的结果通过MultiRemote.elementWrapper()变成多路元素对象,元素上的后续命令同样广播到各实例;元素命令的容错(隐式等待、stale 元素重取、不可交互重试)由 middlewares.ts 的elementErrorHandler配合multiremoteHandler完成; - 事件与自定义命令:
MultiRemoteDriver把on/once/emit等事件 API 广播到每个子实例(multiremote.ts);addCommand与overwriteCommand也会被转发到每个子实例并保留在 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),仅供参考