Electron 自动化测试实战:WebDriver、Playwright 与自定义测试驱动全指南
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
自动化测试是验证 Electron 应用代码行为是否符合预期的高效手段。Electron 官方并未维护一套独立的测试框架,而是依托 Chromium 生态的 WebDriver、Chrome DevTools Protocol(CDP)等标准协议,为开发者提供了 WebdriverIO、Selenium、Playwright 以及基于 Node.js IPC 的自定义测试驱动等多种端到端测试方案。读完本文,你将掌握各方案的环境搭建、配置要点与可运行的测试代码,能够结合当前仓库的源码证据理解它们的底层原理,并为自己的 Electron 应用选型落地。
Electron 测试的总体思路
在 Electron 中运行端到端自动化测试,本质上是让测试框架去驱动一个真实的 Electron 应用进程,像操作浏览器页面一样操作其中的 DOM,同时还要能触达 Electron 特有的主进程能力(如BrowserWindow、dialog等)。由于 Electron 应用是「Chromium + Node.js」的混合体,社区主流的做法有两条技术路线:
- WebDriver 协议路线:由 Chromium 项目维护的 ChromeDriver 为 Electron 提供 WebDriver 服务端实现,上层再套用 WebdriverIO 或 Selenium 框架;
- CDP(Chrome DevTools Protocol)路线:Electron 对 Chrome DevTools Protocol 的支持,让 Playwright 这类基于 CDP 的框架可以直接对 Electron 应用做端到端测试;
- 自研驱动路线:利用 Node.js 内置的 IPC-over-STDIO 能力,在测试套件与被测应用之间建立自定义消息协议,开销低、可控性强。
官方说明:Electron 并不主动维护自有的测试解决方案("Electron doesn't actively maintain its own testing solution"),因此本条指南(docs/tutorial/automated-testing.md)介绍的是几种在 Electron 应用上运行端到端测试的通用方式。
路线一:使用 WebDriver 接口
WebDriver 是跨浏览器自动化测试的开源工具标准,提供网页导航、用户输入模拟、JavaScript 执行等能力。ChromeDriver 是 Chromium 项目与 WebDriver 团队共同开发的独立服务器,实现了 Chromium 上的 WebDriver wire 协议。Electron 同样能通过它被驱动,因为 Chromium 渲染层在协议层面是一致的。
采用 WebDriver 方案搭建测试有两种主流选择,下面分别展开。
子方案 A:使用 WebdriverIO(WDIO)
WebdriverIO 是一个基于 Node.js 的测试自动化框架,生态中提供大量 reporter、service 等插件帮助组装测试环境。如果你已经有一套现成的 WebdriverIO 配置,建议按 WebdriverIO 官方文档中 Electron 桌面测试的配置方式升级依赖并校验现有配置。
安装测试运行器
在项目根目录运行 starter 工具包即可完成初始化:
npm init wdio@latest ./该命令会启动一个配置向导,帮你拼装出合适的测试配置、安装全部依赖,并生成wdio.conf.js配置文件。在向导第一个问题"What type of testing would you like to do?"中,务必选择"Desktop Testing - of Electron Applications"选项。
将 WDIO 连接到 Electron 应用
向导结束后,wdio.conf.js应大致包含以下内容:
export const config = { // ... services: ['electron'], capabilities: [{ browserName: 'electron', 'wdio:electronServiceOptions': { // 如果你使用 Electron Forge 或 electron-builder,WebdriverIO // 可以自动找到打包后的应用;否则需要在此显式指定,例如: // appBinaryPath: './path/to/bundled/application.exe', appArgs: ['foo', 'bar=baz'] } }] // ... }要点说明:
services: ['electron']启用 WDIO 的 Electron 服务插件,由它负责拉起和关闭你的应用进程;appBinaryPath用于指定打包后的应用二进制路径;使用 Electron Forge / electron-builder 时通常无需手填;appArgs会作为命令行参数透传给应用进程,适合用来注入测试专用参数(如上面示例中的foo、bar=baz)。应用主进程可通过process.argv读取,这一机制在你的应用里同样适用于区分"测试模式"与"生产模式"。
编写测试
测试中可直接使用 WebdriverIO API 与屏幕上的元素交互。框架提供了丰富的自定义 "matchers"(断言匹配器),让应用状态的断言非常简单。例如模拟键盘输入并断言页面统计值:
import { browser, $, expect } from '@wdio/globals' describe('keyboard input', () => { it('should detect keyboard input', async () => { await browser.keys(['y', 'o']) await expect($('keypress-count')).toHaveText('YO') }) })更进一步,WebdriverIO 允许你通过browser.electron.execute进入Electron 主进程,调用 Electron API 获取应用静态信息或触发主进程能力。回调函数运行在主进程中,参数依次为require('electron')的返回对象以及你传入的额外参数:
import { browser } from '@wdio/globals' describe('trigger message modal', async () => { it('message modal can be triggered from a test', async () => { await browser.electron.execute( (electron, param1, param2, param3) => { const appWindow = electron.BrowserWindow.getFocusedWindow() electron.dialog.showMessageBox(appWindow, { message: 'Hello World!', detail: `${param1} + ${param2} + ${param3} = ${param1 + param2 + param3}` }) }, 1, 2, 3 ) }) })这段测试直接复用了仓库中 BrowserWindow 相关文档 与 dialog API 描述的能力:先通过BrowserWindow.getFocusedWindow()拿到当前聚焦窗口,再以它为父窗口弹出dialog.showMessageBox。也就是说,测试层可以用与渲染层完全等价的方式去验证主进程 UI 行为。
运行测试
$ npx wdio run wdio.conf.jsWebdriverIO 会自动帮你启动和关闭被测应用,无需手工清理进程。
关于 Mock Electron API 以及更多有用资源的说明,可进一步查阅 WebdriverIO 官方桌面测试(Electron)文档。
子方案 B:使用 Selenium
Selenium 是面向多种语言暴露 WebDriver API 绑定的网页自动化框架,其 Node.js 绑定发布为 NPM 上的selenium-webdriver包。
运行 ChromeDriver 服务器
要配合 Electron 使用 Selenium,需要先拿到 Electron 对应的electron-chromedriver二进制并启动它:
npm install --save-dev electron-chromedriver ./node_modules/.bin/chromedriver Starting ChromeDriver (v2.10.291558) on port 9515 Only local connections are allowed.记下终端输出的端口号9515,后续步骤要用。
electron-chromedriver并非临时概念,它是 Electron 发布流程中随版本一起产出的正式制品。在本仓库中可以看到相关证据:
- script/release/release-assets.json 定义了各平台发布产物清单,其中就包含
chromedriver-${version}-darwin-x64.zip、chromedriver-${version}-linux-arm64.zip、chromedriver-${version}-win32-x64.zip等,覆盖 macOS、Linux、Windows 的 x64 / arm64 / mas 变体; - script/verify-chromedriver.py 是 CI 中用于验证产物可用的脚本:它以子进程方式拉起构建目录下的
chromedriver(darwin/win32/linux 平台名不同,Windows 上为chromedriver.exe),并断言其首行输出匹配正则^Starting ChromeDriver [0-9]+.[0-9]+.[0-9]+.[0-9]+ .* on port [0-9]+$,即能正常初始化监听端口。
因此在版本匹配上,建议安装与你的 Electron 版本一致的electron-chromedriver,其版本号与 Electron 版本一一对应。
将 Selenium 连接到 ChromeDriver
接下来把 Selenium 装进项目:
npm install --save-dev selenium-webdriverselenium-webdriver配合 Electron 的用法与普通网站基本一致,唯一区别是你必须手动指定 ChromeDriver 的地址以及Electron 应用二进制的位置:
const webdriver = require('selenium-webdriver') const driver = new webdriver.Builder() // The "9515" is the port opened by ChromeDriver. .usingServer('http://localhost:9515') .withCapabilities({ 'goog:chromeOptions': { // Here is the path to your Electron binary. binary: '/Path-to-Your-App.app/Contents/MacOS/Electron' } }) .forBrowser('chrome') // note: use .forBrowser('electron') for selenium-webdriver <= 3.6.0 .build() driver.get('https://www.google.com') driver.findElement(webdriver.By.name('q')).sendKeys('webdriver') driver.findElement(webdriver.By.name('btnG')).click() driver.wait(() => { return driver.getTitle().then((title) => { return title === 'webdriver - Google Search' }) }, 1000) driver.quit()两个关键点需要注意:
goog:chromeOptions.binary指向 Electron 可执行文件。示例中是 macOS 上打包应用解包后的二进制路径;在开发模式下,也可以指向node_modules/electron/dist/electron可执行文件并配合入口参数使用;forBrowser('electron')仅适用于selenium-webdriver <= 3.6.0的旧版本,新版本请统一使用forBrowser('chrome')。
成功连接后,Selenium 的所有 find / click / wait / quit 等 WebDriver 操作都能照常在 Electron 窗口中工作,因为协议语义完全一致。
路线二:使用 Playwright
Microsoft Playwright 是基于浏览器专属远程调试协议构建的端到端测试框架,类似 Puppeteer headless API,但更偏向端到端测试场景。Playwright 对 Electron 提供实验性支持,其支撑正是Electron 对 Chrome DevTools Protocol(CDP)的支持。
从源码层面可以印证这一机制:在主进程初始化逻辑 shell/browser/electron_browser_main_parts.cc 中,Electron 检测到--remote-debugging-pipe开关时会调用content::DevToolsAgentHost::StartRemoteDebuggingPipeHandler(...),而检测到--remote-debugging-port开关时则调用DevToolsManagerDelegate::StartHttpHandler(),从而在指定端口暴露 CDP HTTP 调试端点。对应的命令行开关在 docs/api/command-line-switches.md 中有明确记录:--remote-debugging-port=port会在指定端口启用基于 HTTP 的远程调试。此外,仓库自身的测试套件(如 spec/chromium-spec.ts)中就大量使用了ChildProcess.spawn(electronPath, ['--remote-debugging-pipe'], ...)与--remote-debugging-port=0这样的方式拉起被测实例,从实践侧再次验证了这两条 CDP 通道的可用性。
安装依赖
Playwright 自带专为端到端测试设计的测试运行器,通过你惯用的 Node.js 包管理器安装即可:
npm install --save-dev @playwright/test依赖提示:本教程编写时对应
@playwright/test@1.52.0。Playwright 的 API 演进较快,升级版本前请关注其 release notes,确认下方代码是否受影响。
编写测试
Playwright 通过_electron.launchAPI以开发模式启动你的应用。要让该 API 指向你的应用,只需传入主进程入口文件的路径(这里的main.js,即 tutorial-2-first-app.md 中创建的应用入口):
import { test, _electron as electron } from '@playwright/test' test('launch app', async () => { const electronApp = await electron.launch({ args: ['.'] }) // close app await electronApp.close() })启动后你将拿到一个 Playwright 的ElectronApp实例。该类非常强大,例如可以访问主进程模块,在应用的主进程中执行任意代码:
import { test, _electron as electron } from '@playwright/test' test('get isPackaged', async () => { const electronApp = await electron.launch({ args: ['.'] }) const isPackaged = await electronApp.evaluate(async ({ app }) => { // This runs in Electron's main process, parameter here is always // the result of the require('electron') in the main app script. return app.isPackaged }) console.log(isPackaged) // false (because we're in development mode) // close app await electronApp.close() })electronApp.evaluate的回调运行在 Electron 主进程中,其参数永远是主进程脚本require('electron')的结果。这里开发模式下app.isPackaged返回false这一事实,对应 docs/tutorial/application-distribution.md 与 app 模块文档 中关于打包状态的说明。
ElectronApp还能从 ElectronBrowserWindow实例创建独立的Page对象。例如抓取第一个窗口并保存截图:
import { test, _electron as electron } from '@playwright/test' test('save screenshot', async () => { const electronApp = await electron.launch({ args: ['.'] }) const window = await electronApp.firstWindow() await window.screenshot({ path: 'intro.png' }) // close app await electronApp.close() })把上面的能力综合起来,用 Playwright 测试运行器创建一个带断言的单测文件example.spec.js:
import { test, expect, _electron as electron } from '@playwright/test' test('example test', async () => { const electronApp = await electron.launch({ args: ['.'] }) const isPackaged = await electronApp.evaluate(async ({ app }) => { // This runs in Electron's main process, parameter here is always // the result of the require('electron') in the main app script. return app.isPackaged }) expect(isPackaged).toBe(false) // Wait for the first BrowserWindow to open // and return its Page object const window = await electronApp.firstWindow() await window.screenshot({ path: 'intro.png' }) // close app await electronApp.close() })随后运行npx playwright test,你应当能在终端看到测试通过,并在文件系统上看到生成的intro.png截图:
☁ $ npx playwright test Running 1 test using 1 worker ✓ example.spec.js:4:1 › example test (1s)两条实用的工程信息:
- Playwright Test 会自动匹配所有符合
.*(test|spec)\.(js|ts|mjs)正则的文件,你可以在 Playwright Test 配置中自定义testMatch; - 它开箱即用地支持 TypeScript,无需额外转译配置。
路线三:自定义测试驱动
如果你追求更低的开销,并希望向测试套件暴露自定义方法,可以绕过 WebDriver / CDP,利用 Node.js 内置的IPC-over-STDIO机制自研一个轻量测试驱动。代价是需要为应用额外编写少量配套代码。这一方案特别适合仓库内部自带大量集成测试的场景——本仓库的 spec 目录(见下)正是这类思路的规模化实践。
进程拉起与消息协议
测试套件通过 Node.js 的child_processAPI 拉起 Electron 进程,并利用stdio数组中最后一个'ipc'通道与子进程通信:
const electronPath = require('electron') const childProcess = require('node:child_process') // spawn the process const env = { /* ... */ } const stdio = ['inherit', 'inherit', 'inherit', 'ipc'] const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env }) // listen for IPC messages from the app appProcess.on('message', (msg) => { // ... }) // send an IPC message to the app appProcess.send({ my: 'message' })应用侧(主进程脚本main.js)通过 Node.js 的processAPI 监听消息并回复:
// listen for messages from the test suite process.on('message', (msg) => { // ... }) // send a message to the test suite process.send({ my: 'message' })至此,测试套件与 Electron 应用之间已经能借助appProcess对象双向通信。
封装一个高层的 TestDriver
裸的appProcess用起来不便,通常的做法是把它包装成一个提供高层函数的驱动对象。先创建TestDriver类,内部实现一个简单的 RPC 机制:
class TestDriver { constructor ({ path, args, env }) { this.rpcCalls = [] // start child process env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env }) // handle rpc responses this.process.on('message', (message) => { // pop the handler const rpcCall = this.rpcCalls[message.msgId] if (!rpcCall) return this.rpcCalls[message.msgId] = null // reject/resolve if (message.reject) rpcCall.reject(message.reject) else rpcCall.resolve(message.resolve) }) // wait for ready this.isReady = this.rpc('isReady').catch((err) => { console.error('Application failed to start', err) this.stop() process.exit(1) }) } // simple RPC call // to use: driver.rpc('method', 1, 2, 3).then(...) async rpc (cmd, ...args) { // send rpc request const msgId = this.rpcCalls.length this.process.send({ msgId, cmd, args }) return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject })) } stop () { this.process.kill() } } module.exports = { TestDriver }设计要点:
- 构造时通过
env.APP_TEST_DRIVER = 1环境变量通知应用进入"被测试驱动"模式; rpc(cmd, ...args)为每个请求分配自增msgId,把resolve/reject按msgId暂存进rpcCalls数组,消息回来时按msgId取出对应处理器;- 构造函数末尾立刻发起
isReadyRPC,把返回的 Promise 保存在this.isReady上,方便测试框架before阶段等待应用就绪;若应用启动失败则打印错误、停止进程并以退出码 1 结束。
应用侧:接收 RPC 调用
在应用的main.js中编写一个简单的 RPC 分发处理器:
const METHODS = { isReady () { // do any setup needed return true } // define your RPC-able methods here } const onMessage = async ({ msgId, cmd, args }) => { let method = METHODS[cmd] if (!method) method = () => new Error('Invalid method: ' + cmd) try { const resolve = await method(...args) process.send({ msgId, resolve }) } catch (err) { const reject = { message: err.message, stack: err.stack, name: err.name } process.send({ msgId, reject }) } } if (process.env.APP_TEST_DRIVER) { process.on('message', onMessage) }METHODS表即"可 RPC 的方法注册表",新测试能力只需在这里追加方法即可。process.env.APP_TEST_DRIVER守卫保证了:只有带该环境变量启动时(即测试场景下)才会挂载消息监听,日常运行不受影响。异常时会把message、stack、name序列化后随reject回传,保证测试侧的报错信息完整可读。
接入任意测试框架
最后,在测试套件里把TestDriver与任意测试自动化框架组合即可。下面的例子使用ava,换成 Jest 或 Mocha 等主流框架也完全可行:
const electronPath = require('electron') const test = require('ava') const { TestDriver } = require('./testDriver') const app = new TestDriver({ path: electronPath, args: ['./app'], env: { NODE_ENV: 'test' } }) test.before(async t => { await app.isReady }) test.after.always('cleanup', async t => { await app.stop() })仓库中的规模化实践
"自研驱动"并非纸上谈兵。查看本仓库的 spec 目录,其中约上百个api-*-spec.ts测试文件构成了 Electron 自身的庞大测试套件(api-app-spec.ts、api-ipc-main-spec.ts、api-menu-spec.ts 等)。这些用例的启动方式(如 spec/chromium-spec.ts 中直接ChildProcess.spawn(electronPath, ['--remote-debugging-pipe'], ...)拉起被测进程)与上文展示的自研驱动思路一脉相承,可作为深入阅读"如何给 Electron 应用写大规模端到端测试"的真实范本。
四种方案的横向对比与选型建议
| 方案 | 测试入口 | 是否需要额外服务 | 主进程可访问性 | 额外应用代码 | 推荐场景 |
|---|---|---|---|---|---|
| WebdriverIO | npx wdio run wdio.conf.js | 由 electron service 自动管理 | browser.electron.execute可进主进程 | 少(打包后可自动发现应用) | 已有 WDIO 生态、需要丰富 reporter/plugin 的团队 |
| Selenium | 自写 Node 脚本 / 测试框架 | 需手动启动electron-chromedriver并记住端口 | 不可直接进主进程,面向页面层 | 少 | 已有 Selenium 语言绑定 / 多语言团队 |
| Playwright | npx playwright test | 自动(经 CDP 管道启动) | electronApp.evaluate/firstWindow() | 少(直接开发模式启动) | 偏好开箱即用 runner、需要截图/页面对象模型 |
| 自定义驱动 | 接 ava / Jest / Mocha | 无(IPC-over-STDIO) | 通过 RPC 完全自定义 | 需写TestDriver与主进程 handler | 需要自定义方法、追求低开销、大规模内部测试 |
几个实用的通用建议:
- 开发模式下 Playwright 的
app.isPackaged === false、Selenium 指向打包二进制等行为差异,决定了测试写的是"开发态"还是"发布态"路径;如需覆盖打包后行为,请基于 application-distribution.md 所述流程产出应用包后再测; - 无论走哪条路线,建议在测试脚本中显式注入与常规运行不同的参数或环境变量(如
appArgs、NODE_ENV: 'test'、APP_TEST_DRIVER=1),以避免测试污染真实数据; - Electron 各版本对 CDP 的支持细节与 Playwright 的实验性适配会随版本演进,锁定版本并随 Electron 升级同步回归测试是最稳妥的做法。
小结
自动化测试能高效验证 Electron 应用"代码行为符合预期":WebdriverIO 与 Selenium 走 WebDriver + ChromeDriver 路线,聚焦页面交互并可触及主进程;Playwright 借道 Electron 对 CDP 的内建支持(源码见 shell/browser/electron_browser_main_parts.cc)提供流畅的 runner 体验;自定义测试驱动则以 IPC-over-STDIO 换来最低开销与最大定制空间。本文四种方案均配有可直接运行的示例代码与对应的仓库源码佐证,你可以据此为自己的 Electron 项目搭建一套可持续演进的端到端测试体系。
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考