跳转到主内容

自动化测试

测试自动化是一种验证您的应用是否如你预期那样正常工作的有效方法。 Electron已然不再维护自己的测试解决方案, 本指南将会介绍几种方法,让您可以在 Electron 应用上进行端到端自动测试。

使用 WebDriver 接口

引自 ChromeDriver - WebDriver for Chrome:

WebDriver 是一款开源的支持多浏览器的自动化测试工具。 它提供了操作网页、用户输入、JavaScript 执行等能力。 ChromeDriver 是一个实现了 WebDriver 与 Chromium 联接协议的独立服务。 它也是由开发了 Chromium 和 WebDriver 的团队开发的。

有几种方法可以使用 WebDriver 设置测试。

使用 WebdriverIO

WebdriverIO (WDIO) 是一个自动化测试框架,它提供了一个 Node.js 软件包用于测试 Web 驱动程序。 它的生态系统还包括各种插件(例如报告器和服务) ,可以帮助你把测试设置放在一起。

If you already have an existing WebdriverIO setup, it is recommended to update your dependencies and validate your existing configuration with how it is outlined in the docs.

Install the test runner

If you don't use WebdriverIO in your project yet, you can add it by running the starter toolkit in your project root directory:

npm init wdio@latest ./

This starts a configuration wizard that helps you put together the right setup, installs all necessary packages, and generates a wdio.conf.js configuration file. Make sure to select "Desktop Testing - of Electron Applications" on one of the first questions asking "What type of testing would you like to do?".

将 WDIO 连接到 Electron 应用程序

After running the configuration wizard, your wdio.conf.js should include roughly the following content:

wdio.conf.js
export const config = {
// ...
services: ['electron'],
capabilities: [{
browserName: 'electron',
'wdio:electronServiceOptions': {
// WebdriverIO can automatically find your bundled application
// if you use Electron Forge or electron-builder, otherwise you
// can define it here, e.g.:
// appBinaryPath: './path/to/bundled/application.exe',
appArgs: ['foo', 'bar=baz']
}
}]
// ...
}

编写测试

Use the WebdriverIO API to interact with elements on the screen. The framework provides custom "matchers" that make asserting the state of your application easy, e.g.:

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')
})
})

Furthermore, WebdriverIO allows you to access Electron APIs to get static information about your application:

import { browser, $, expect } from '@wdio/globals'

describe('when the make smaller button is clicked', () => {
it('should decrease the window height and width by 10 pixels', async () => {
const boundsBefore = await browser.electron.browserWindow('getBounds')
expect(boundsBefore.width).toEqual(210)
expect(boundsBefore.height).toEqual(310)

await $('.make-smaller').click()
const boundsAfter = await browser.electron.browserWindow('getBounds')
expect(boundsAfter.width).toEqual(200)
expect(boundsAfter.height).toEqual(300)
})
})

or to retrieve other Electron process information:

import fs from 'node:fs'
import path from 'node:path'
import { browser, expect } from '@wdio/globals'

const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), { encoding: 'utf-8' }))
const { name, version } = packageJson

describe('electron APIs', () => {
it('should retrieve app metadata through the electron API', async () => {
const appName = await browser.electron.app('getName')
expect(appName).toEqual(name)
const appVersion = await browser.electron.app('getVersion')
expect(appVersion).toEqual(version)
})

it('should pass args through to the launched application', async () => {
// custom args are set in the wdio.conf.js file as they need to be set before WDIO starts
const argv = await browser.electron.mainProcess('argv')
expect(argv).toContain('--foo')
expect(argv).toContain('--bar=baz')
})
})

运行测试

执行命令:

$ npx wdio run wdio.conf.js

WebdriverIO helps launch and shut down the application for you.

More documentation

Find more documentation on Mocking Electron APIs and other useful resources in the official WebdriverIO documentation.

使用 Selenium

Selenium 是一个Web自动化框架,以多种语言公开与 WebDriver API 的绑定方式。 Node.js 环境下, 可以通过 NPM 安装 selenium-webdriver 包来使用此框架。

运行 ChromeDriver 服务

为了与 Electron 一起使用 Selenium ,你需要下载 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.

Remember the port number 9515, which will be used later.

将 Selenium 连接到 ChromeDriver

接下来,把 Selenium 安装到你的项目中:

npm install --save-dev selenium-webdriver

在 Electron 下使用 selenium-webdriver 和其平时的用法并没有大的差异,只是你需要手动设置如何连接 ChromeDriver,以及 Electron 应用的查找路径:

test.js
const webdriver = require('selenium-webdriver')
const driver = new webdriver.Builder()
// 端口号 "9515" 是被 ChromeDriver 开启的.
.usingServer('http://localhost:9515')
.withCapabilities({
'goog:chromeOptions': {
// 这里填您的Electron二进制文件路径。
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()

使用 Playwright

Microsoft Playwright 是一个端到端的测试框架,使用浏览器特定的远程调试协议架构,类似于 Puppeteer 的无头 Node.js API,但面向端到端测试。 Playwright 通过 Electron 支持 [Chrome DevTools 协议][] (CDP) 获得实验性的 Electron 支持。

安装依赖项

您可以通过 Node.js 包管理器安装 Playwright。 It comes with its own test runner, which is built for end-to-end testing:

npm install --save-dev @playwright/test

::: 依赖注意事项

This tutorial was written with @playwright/test@1.41.1. Check out Playwright's releases page to learn about changes that might affect the code below.

:::

编写测试

Playwright通过 _electron.launch API在开发模式下启动您的应用程序。 要将此 API 指向 Electron 应用,可以将路径传递到主进程入口点(此处为 main.js)。

const { test, _electron: electron } = require('@playwright/test')

test('launch app', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
// close app
await electronApp.close()
})

在此之后,您将可以访问到 Playwright 的 ElectronApp 类的一个实例。 这是一个功能强大的类,可以访问主进程模块,例如:

const { test, _electron: electron } = require('@playwright/test')

test('get isPackaged', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
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(因为我们处在开发环境)
// 关闭应用程序
await electronApp.close()
})

It can also create individual Page objects from Electron BrowserWindow instances. 例如,获取第一个 BrowserWindow 并保存一个屏幕截图:

const { test, _electron: electron } = require('@playwright/test')

test('save screenshot', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
const window = await electronApp.firstWindow()
await window.screenshot({ path: 'intro.png' })
// close app
await electronApp.close()
})

Putting all this together using the Playwright test-runner, let's create a example.spec.js test file with a single test and assertion:

example.spec.js
const { test, expect, _electron: electron } = require('@playwright/test')

test('example test', async () => {
const electronApp = await electron.launch({ args: ['.'] })
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// 在 Electron 的主进程运行,这里的参数总是
// 主程序代码中 require('electron') 的返回结果。
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 运行 Playwright 测试。 您应该在您的控制台中看到测试通过,并在您的文件系统上看到一个屏幕截图 intro.png

☁  $ npx playwright test

Running 1 test using 1 worker

✓ example.spec.js:4:1 › example test (1s)
info

PlayWright Test 将自动运行与正则表达式 .*(test|spec)\.(js|ts|mjs) 匹配的所有文件。 You can customize this match in the Playwright Test configuration options. It also works with TypeScript out of the box.

:::延伸阅读

Check out Playwright's documentation for the full Electron and ElectronApplication class APIs.

:::

使用自定义测试驱动

当然,也可以使用node的内建IPC STDIO来编写自己的自定义驱动。 自定义测试驱动程序需要您写额外的应用代码,但是有较低的开销,让您 在您的测试套装上显示自定义方法。

我们将用 Node.js 的 child_process API 来创建一个自定义驱动。 测试套件将生成 Electron 子进程,然后建立一个简单的消息传递协议。

testDriver.js
const childProcess = require('node:child_process')
const electronPath = require('electron')

// 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) => {
// ...
})

// 向应用发送IPC消息
appProcess.send({ my: 'message' })

在 Electron 应用程序中,您可以使用 Node.js 的 process API 监听消息并发送回复:

main.js
// 监听测试套件发送过来的消息
process.on('message', (msg) => {
// ...
})

// 发送一条消息到测试套件
process.send({ my: 'message' })

现在,我们可以使用appProcess 对象从测试套件到Electron应用进行通讯。

为方便起见,您可能希望将 appProcess 包装在一个提供更高级功能的驱动程序对象中。 下面是一个示例。 让我们从创建一个 TestDriver 类开始:

testDriver.js
class TestDriver {
constructor ({ path, args, env }) {
this.rpcCalls = []

// 启动子进程
env.APP_TEST_DRIVER = 1 // 让应用知道它应当侦听信息
this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })

// 处理RPC回复
this.process.on('message', (message) => {
// 弹出处理器
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)
})

// 等待准备完毕
this.isReady = this.rpc('isReady').catch((err) => {
console.error('Application failed to start', err)
this.stop()
process.exit(1)
})
}

// 简单 RPC 回调
// 可以使用: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 }

然后,在您的应用代码中,可以编写一个简单的处理程序来接收 RPC 调用:

main.js
const METHODS = {
isReady () {
// 进行任何需要的初始化
return true
}
// 在这里定义可做 RPC 调用的方法
}

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)
}

然后,在您的测试套件中,您可以使用TestDriver 类用你选择的自动化测试框架。 下面的示例使用 ava,但其他流行的选择,如Jest 或者Mocha 也可以:

test.js
const test = require('ava')
const electronPath = require('electron')
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()
})

[Chrome DevTools 协议]: https://chromedevtools. github. io/devtools-protocol/