Skip to main content

自动化测试

¥Automated Testing

测试自动化是验证应用代码是否按预期工作的有效方法。虽然 Electron 没有积极维护自己的测试解决方案,但本指南将介绍几种在 Electron 应用上运行端到端自动化测试的方法。

¥Test automation is an efficient way of validating that your application code works as intended. While Electron doesn't actively maintain its own testing solution, this guide will go over a couple ways you can run end-to-end automated tests on your Electron app.

使用 WebDriver 接口

¥Using the WebDriver interface

来自 ChromeDriver - 适用于 Chrome 的网络驱动程序

¥From ChromeDriver - WebDriver for Chrome:

WebDriver 是一个开源工具,用于跨多种浏览器自动测试 Web 应用。它提供了导航网页、用户输入、JavaScript 执行等功能。ChromeDriver 是一个独立的服务器,它为 Chromium 实现 WebDriver 的有线协议。它由 Chromium 和 WebDriver 团队的成员开发。

¥WebDriver is an open source tool for automated testing of web apps across many browsers. It provides capabilities for navigating to web pages, user input, JavaScript execution, and more. ChromeDriver is a standalone server which implements WebDriver's wire protocol for Chromium. It is being developed by members of the Chromium and WebDriver teams.

你可以通过多种方式使用 WebDriver 设置测试。

¥There are a few ways that you can set up testing using WebDriver.

使用 WebdriverIO

¥With WebdriverIO

网络驱动 IO (WDIO) 是一个测试自动化框架,提供用于使用 WebDriver 进行测试的 Node.js 包。其生态系统还包括各种插件(例如报告器和服务),可以帮助你组合测试设置。

¥WebdriverIO (WDIO) is a test automation framework that provides a Node.js package for testing with WebDriver. Its ecosystem also includes various plugins (e.g. reporter and services) that can help you put together your test setup.

如果你已有 WebdriverIO 设置,建议更新你的依赖并验证现有配置是否与 文档中概述 相同。

¥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

如果你的项目中尚未使用 WebdriverIO,你可以通过运行项目根目录中的入门工具包来添加它:

¥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 ./

这将启动一个配置向导,帮助你进行正确的设置、安装所有必需的软件包并生成 wdio.conf.js 配置文件。确保选择“桌面测试 - Electron 应用 "关于第一个问题之一" 你想要进行什么类型的测试?”。

¥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 应用

¥Connect WDIO to your Electron app

运行配置向导后,你的 wdio.conf.js 应大致包含以下内容:

¥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']
}
}]
// ...
}

编写你的测试

¥Write your tests

使用 WebdriverIO API 与屏幕上的元素进行交互。该框架提供了自定义 "matchers",可以轻松断言应用的状态,例如:

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

此外,WebdriverIO 允许你访问 Electron API 以获取有关应用的静态信息:

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

或检索其他 Electron 进程信息:

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

运行你的测试

¥Run your tests

运行测试:

¥To run your tests:

$ npx wdio run wdio.conf.js

WebdriverIO 可帮助你启动和关闭应用。

¥WebdriverIO helps launch and shut down the application for you.

更多文档

¥More documentation

官方 WebdriverIO 文档 中查找有关 Mocking Electron API 的更多文档和其他有用的资源。

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

含硒

¥With Selenium

是一个 Web 自动化框架,它公开了与多种语言的 WebDriver API 的绑定。它们的 Node.js 绑定可在 NPM 上的 selenium-webdriver 包下获取。

¥Selenium is a web automation framework that exposes bindings to WebDriver APIs in many languages. Their Node.js bindings are available under the selenium-webdriver package on NPM.

运行 ChromeDriver 服务器

¥Run a ChromeDriver server

为了将 Selenium 与 Electron 一起使用,你需要下载 electron-chromedriver 二进制文件并运行它:

¥In order to use Selenium with Electron, you need to download the electron-chromedriver binary, and run it:

npm install --save-dev electron-chromedriver
./node_modules/.bin/chromedriver
Starting ChromeDriver (v2.10.291558) on port 9515
Only local connections are allowed.

记住端口号 9515,稍后会用到。

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

将 Selenium 连接到 ChromeDriver

¥Connect Selenium to ChromeDriver

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

¥Next, install Selenium into your project:

npm install --save-dev selenium-webdriver

selenium-webdriver 在 Electron 中的使用与普通网站相同,只是你必须手动指定如何连接 ChromeDriver 以及在哪里找到 Electron 应用的二进制文件:

¥Usage of selenium-webdriver with Electron is the same as with normal websites, except that you have to manually specify how to connect ChromeDriver and where to find the binary of your Electron app:

test.js
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

¥Using Playwright

微软 Playwright 是一个使用特定于浏览器的远程调试协议构建的端到端测试框架,类似于 Puppeteer headless Node.js API,但面向端到端测试。Playwright 通过 Electron 对 Chrome 开发者工具协议 (CDP) 的支持获得了实验性 Electron 支持。

¥Microsoft Playwright is an end-to-end testing framework built using browser-specific remote debugging protocols, similar to the Puppeteer headless Node.js API but geared towards end-to-end testing. Playwright has experimental Electron support via Electron's support for the Chrome DevTools Protocol (CDP).

安装依赖

¥Install dependencies

你可以通过你首选的 Node.js 包管理器安装 Playwright。它带有自己的 测试运行器,专为端到端测试而构建:

¥You can install Playwright through your preferred Node.js package manager. It comes with its own test runner, which is built for end-to-end testing:

npm install --save-dev @playwright/test
依赖

本教程是用 @playwright/test@1.41.1 编写的。查看 Playwright 的作品 页面以了解可能影响以下代码的更改。

¥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.

编写你的测试

¥Write your tests

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

¥Playwright launches your app in development mode through the _electron.launch API. To point this API to your Electron app, you can pass the path to your main process entry point (here, it is 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 类的实例。这是一个功能强大的类,可以访问主进程模块,例如:

¥After that, you will access to an instance of Playwright's ElectronApp class. This is a powerful class that has access to main process modules for example:

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 实例创建单独的 对象。例如,要抓取第一个 BrowserWindow 并保存屏幕截图:

¥It can also create individual Page objects from Electron BrowserWindow instances. For example, to grab the first BrowserWindow and save a screenshot:

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 测试文件:

¥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 }) => {
// 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 屏幕截图。

¥Then, run Playwright Test using npx playwright test. You should see the test pass in your console, and have an intro.png screenshot on your filesystem.

☁  $ 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 Test will automatically run any files matching the .*(test|spec)\.(js|ts|mjs) regex. You can customize this match in the Playwright Test configuration options. It also works with TypeScript out of the box.

进一步阅读

查看 Playwright 的文档,了解完整的 ElectronElectronApplication 类 API。

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

使用自定义测试驱动程序

¥Using a custom test driver

还可以使用 Node.js 的内置 IPC-over-STDIO 编写你自己的自定义驱动程序。自定义测试驱动程序要求你编写额外的应用代码,但开销较低,并且允许你向测试套件公开自定义方法。

¥It's also possible to write your own custom driver using Node.js' built-in IPC-over-STDIO. Custom test drivers require you to write additional app code, but have lower overhead and let you expose custom methods to your test suite.

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

¥To create a custom driver, we'll use Node.js' child_process API. The test suite will spawn the Electron process, then establish a simple messaging protocol:

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

// send an IPC message to the app
appProcess.send({ my: 'message' })

在 Electron 应用中,你可以使用 Node.js process API 监听消息并发送响应:

¥From within the Electron app, you can listen for messages and send replies using the Node.js process API:

main.js
// listen for messages from the test suite
process.on('message', (msg) => {
// ...
})

// send a message to the test suite
process.send({ my: 'message' })

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

¥We can now communicate from the test suite to the Electron app using the appProcess object.

为了方便起见,你可能希望将 appProcess 封装在提供更多高级功能的驱动程序对象中。以下是如何执行此操作的示例。让我们首先创建一个 TestDriver 类:

¥For convenience, you may want to wrap appProcess in a driver object that provides more high-level functions. Here is an example of how you can do this. Let's start by creating a TestDriver class:

testDriver.js
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 调用:

¥In your app code, can then write a simple handler to receive RPC calls:

main.js
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)也可以使用:

¥Then, in your test suite, you can use your TestDriver class with the test automation framework of your choosing. The following example uses ava, but other popular choices like Jest or Mocha would work as well:

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