自动化测试
测试自动化是验证应用程序代码是否按预期工作的一种有效方法。 尽管 Electron 没有积极维护自己的测试解决方案,但本指南将介绍几种在 Electron 应用程序上运行端到端自动化测试的方法。
使用 WebDriver 接口
摘自 ChromeDriver - 面向 Chrome 的 WebDriver
WebDriver 是一个开源工具,用于跨多种浏览器对 Web 应用进行自动化测试。 它提供了导航到网页、用户输入、JavaScript 执行等功能。 ChromeDriver 是一个独立服务器,实现了 WebDriver 针对 Chromium 的有线协议。 它由 Chromium 和 WebDriver 团队的成员开发。
有几种方法可以使用 WebDriver 设置测试。
使用 WebdriverIO
WebdriverIO (WDIO) 是一个测试自动化框架,提供了一个用于使用 WebDriver 进行测试的 Node.js 包。 它的生态系统还包括各种插件 (例如报告器和服务),可以帮助您搭建测试环境。
如果您已有 WebdriverIO 环境,建议更新依赖项并按照文档中概述的方式验证现有配置。
安装测试运行器
如果您的项目中尚未开始使用 WebdriverIO,可以在项目根目录运行 starter toolkit 来添加它
- npm
- Yarn
npm init wdio@latest ./
yarn create wdio@latest ./
这将启动一个配置向导,帮助您搭建合适的设置,安装所有必需的包,并生成一个 wdio.conf.js
配置文件。 在最初询问 "您想进行哪种类型的测试?" 的问题中,请务必选择 "桌面应用测试 - Electron 应用"。
将 WDIO 连接到您的 Electron 应用
运行配置向导后,您的 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']
}
}]
// ...
}
编写测试
使用 WebdriverIO API 与屏幕上的元素交互。 该框架提供了自定义的“匹配器”,使断言应用程序状态变得容易,例如:
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 允许您访问 Electron API 以获取有关应用程序的静态信息
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)
})
})
或检索其他 Electron 进程信息
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 会帮助您启动和关闭应用程序。
更多文档
在官方 WebdriverIO 文档中查找关于模拟 Electron API 和其他有用资源的更多文档。
使用 Selenium
Selenium 是一个 Web 自动化框架,提供了多种语言的 WebDriver API 绑定。 其 Node.js 绑定可在 NPM 上的 selenium-webdriver
包下找到。
运行 ChromeDriver 服务器
为了将 Selenium 与 Electron 一起使用,您需要下载 electron-chromedriver
二进制文件并运行它
- npm
- Yarn
npm install --save-dev electron-chromedriver
./node_modules/.bin/chromedriver
Starting ChromeDriver (v2.10.291558) on port 9515
Only local connections are allowed.
yarn add --dev electron-chromedriver
./node_modules/.bin/chromedriver
Starting ChromeDriver (v2.10.291558) on port 9515
Only local connections are allowed.
记住端口号 9515
,稍后将用到它。
将 Selenium 连接到 ChromeDriver
接下来,在您的项目中安装 Selenium
- npm
- Yarn
npm install --save-dev selenium-webdriver
yarn add --dev selenium-webdriver
selenium-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()
使用 Playwright
Microsoft Playwright 是一个端到端测试框架,使用特定于浏览器的远程调试协议构建,类似于 Puppeteer 无头 Node.js API,但更侧重于端到端测试。 Playwright 通过 Electron 对 Chrome DevTools Protocol (CDP) 的支持,提供了实验性的 Electron 支持。
安装依赖项
您可以通过您喜欢的 Node.js 包管理器安装 Playwright。 它自带了一个为端到端测试构建的测试运行器
- npm
- Yarn
npm install --save-dev @playwright/test
yarn add --dev @playwright/test
本教程使用 @playwright/test@1.41.1
编写。 查看Playwright 的发布说明页面,了解可能影响下方代码的更改。
编写测试
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 (because we're in development mode)
// close app
await electronApp.close()
})
它还可以从 Electron BrowserWindow 实例创建单独的 Page 对象。 例如,抓取第一个 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()
})
结合使用 Playwright 测试运行器,让我们创建一个包含单个测试和断言的 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 }) => {
// 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
运行 Playwright 测试。 您应该会在控制台中看到测试通过,并且在文件系统中会有一个 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 测试配置选项中自定义此匹配项。 它也开箱即用地支持 TypeScript。
查看 Playwright 的文档,了解完整的 Electron 和 ElectronApplication 类 API。
使用自定义测试驱动程序
还可以使用 Node.js 内置的基于 STDIO 的 IPC 来编写自己的自定义驱动程序。 自定义测试驱动程序需要编写额外的应用代码,但开销较低,并允许您向测试套件公开自定义方法。
要创建一个自定义驱动程序,我们将使用 Node.js 的 child_process
API。 测试套件将生成 Electron 进程,然后建立一个简单的消息协议
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) => {
// ...
})
// send an IPC message to the app
appProcess.send({ my: 'message' })
在 Electron 应用内部,您可以使用 Node.js process
API 监听消息并发送回复
// listen for messages from the test suite
process.on('message', (msg) => {
// ...
})
// send a message to the test suite
process.send({ my: 'message' })
现在我们可以使用 appProcess
对象从测试套件与 Electron 应用进行通信。
为了方便起见,您可能希望将 appProcess
封装在一个提供更高级函数的驱动程序对象中。 这里有一个示例说明如何做到这一点。 让我们首先创建一个 TestDriver
类
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 }
然后,在您的应用代码中,可以编写一个简单的处理程序来接收 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)
}
然后,在您的测试套件中,您可以将 TestDriver
类与您选择的测试自动化框架一起使用。 下面的示例使用 ava
,但其他流行的选择,如 Jest 或 Mocha 也能正常工作
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()
})