自动化测试

测试自动化是一种验证您的应用是否如你预期那样正常工作的有效方法。 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
  • Yarn
  1. npm init wdio@latest ./
  1. yarn create 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

  1. export const config = {
  2. // ...
  3. services: ['electron'],
  4. capabilities: [{
  5. browserName: 'electron',
  6. 'wdio:electronServiceOptions': {
  7. // WebdriverIO can automatically find your bundled application
  8. // if you use Electron Forge or electron-builder, otherwise you
  9. // can define it here, e.g.:
  10. // appBinaryPath: './path/to/bundled/application.exe',
  11. appArgs: ['foo', 'bar=baz']
  12. }
  13. }]
  14. // ...
  15. }

编写测试

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

  1. import { browser, $, expect } from '@wdio/globals'
  2. describe('keyboard input', () => {
  3. it('should detect keyboard input', async () => {
  4. await browser.keys(['y', 'o'])
  5. await expect($('keypress-count')).toHaveText('YO')
  6. })
  7. })

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

  1. import { browser, $, expect } from '@wdio/globals'
  2. describe('when the make smaller button is clicked', () => {
  3. it('should decrease the window height and width by 10 pixels', async () => {
  4. const boundsBefore = await browser.electron.browserWindow('getBounds')
  5. expect(boundsBefore.width).toEqual(210)
  6. expect(boundsBefore.height).toEqual(310)
  7. await $('.make-smaller').click()
  8. const boundsAfter = await browser.electron.browserWindow('getBounds')
  9. expect(boundsAfter.width).toEqual(200)
  10. expect(boundsAfter.height).toEqual(300)
  11. })
  12. })

or to retrieve other Electron process information:

  1. import fs from 'node:fs'
  2. import path from 'node:path'
  3. import { browser, expect } from '@wdio/globals'
  4. const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), { encoding: 'utf-8' }))
  5. const { name, version } = packageJson
  6. describe('electron APIs', () => {
  7. it('should retrieve app metadata through the electron API', async () => {
  8. const appName = await browser.electron.app('getName')
  9. expect(appName).toEqual(name)
  10. const appVersion = await browser.electron.app('getVersion')
  11. expect(appVersion).toEqual(version)
  12. })
  13. it('should pass args through to the launched application', async () => {
  14. // custom args are set in the wdio.conf.js file as they need to be set before WDIO starts
  15. const argv = await browser.electron.mainProcess('argv')
  16. expect(argv).toContain('--foo')
  17. expect(argv).toContain('--bar=baz')
  18. })
  19. })

运行测试

执行命令:

  1. $ 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
  • Yarn
  1. npm install --save-dev electron-chromedriver
  2. ./node_modules/.bin/chromedriver
  3. Starting ChromeDriver (v2.10.291558) on port 9515
  4. Only local connections are allowed.
  1. yarn add --dev electron-chromedriver
  2. ./node_modules/.bin/chromedriver
  3. Starting ChromeDriver (v2.10.291558) on port 9515
  4. Only local connections are allowed.

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

将 Selenium 连接到 ChromeDriver

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

  • npm
  • Yarn
  1. npm install --save-dev selenium-webdriver
  1. yarn add --dev selenium-webdriver

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

test.js

  1. const webdriver = require('selenium-webdriver')
  2. const driver = new webdriver.Builder()
  3. // 端口号 "9515" 是被 ChromeDriver 开启的.
  4. .usingServer('http://localhost:9515')
  5. .withCapabilities({
  6. 'goog:chromeOptions': {
  7. // 这里填您的Electron二进制文件路径。
  8. binary: '/Path-to-Your-App.app/Contents/MacOS/Electron'
  9. }
  10. })
  11. .forBrowser('chrome') // note: use .forBrowser('electron') for selenium-webdriver <= 3.6.0
  12. .build()
  13. driver.get('https://www.google.com')
  14. driver.findElement(webdriver.By.name('q')).sendKeys('webdriver')
  15. driver.findElement(webdriver.By.name('btnG')).click()
  16. driver.wait(() => {
  17. return driver.getTitle().then((title) => {
  18. return title === 'webdriver - Google Search'
  19. })
  20. }, 1000)
  21. driver.quit()

使用 Playwright

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

安装依赖项

您可以通过 Node.js 包管理器安装 Playwright。 Playwright团队推荐使用 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD 环境变量来避免在测试 Electron 软件时进行不必要的浏览器下载。

  • npm
  • Yarn
  1. PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --save-dev playwright
  1. PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn add --dev playwright

Playwright 同时也有自己的测试运行器( Playwright Test ),可用作端到端(E2E)测试。 你也可以在项目中作为开发以来来安装它:

  • npm
  • Yarn
  1. npm install --save-dev @playwright/test
  1. yarn add --dev @playwright/test

::: 依赖注意事项

本教程的编写基于 playwright@1.16.3@playwright/test@1.16.3 查看Playwright 的版本更新 页面以知晓可能会影响到的代码更改。

:::

:::使用第三方测试运行程序的信息

如果您有兴趣使用其他测试运行其(例如 Jest 或 Mocha),请查看 Playwright 的 第三方测试运行器 指南。

:::

编写测试

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

  1. const { _electron: electron } = require('playwright')
  2. const { test } = require('@playwright/test')
  3. test('launch app', async () => {
  4. const electronApp = await electron.launch({ args: ['main.js'] })
  5. // close app
  6. await electronApp.close()
  7. })

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

  1. const { _electron: electron } = require('playwright')
  2. const { test } = require('@playwright/test')
  3. test('get isPackaged', async () => {
  4. const electronApp = await electron.launch({ args: ['main.js'] })
  5. const isPackaged = await electronApp.evaluate(async ({ app }) => {
  6. // 在 Electron 的主进程运行,这里的参数总是
  7. // 主程序代码中 require('electron') 的返回结果。
  8. return app.isPackaged
  9. })
  10. console.log(isPackaged) // false(因为我们处在开发环境)
  11. // 关闭应用程序
  12. await electronApp.close()
  13. })

它还可以从 Electron BrowserWindow 实例创建单独的 Page 对象。 例如,获取第一个 BrowserWindow 并保存一个屏幕截图:

  1. const { _electron: electron } = require('playwright')
  2. const { test } = require('@playwright/test')
  3. test('save screenshot', async () => {
  4. const electronApp = await electron.launch({ args: ['main.js'] })
  5. const window = await electronApp.firstWindow()
  6. await window.screenshot({ path: 'intro.png' })
  7. // 关闭应用程序
  8. await electronApp.close()
  9. })

使用 PlayWright 测试运行器将所有这些组合到一起,让我们创建一个有单个测试和断言的 example.spec.js 测试文件:

example.spec.js

  1. const { _electron: electron } = require('playwright')
  2. const { test, expect } = require('@playwright/test')
  3. test('example test', async () => {
  4. const electronApp = await electron.launch({ args: ['.'] })
  5. const isPackaged = await electronApp.evaluate(async ({ app }) => {
  6. // 在 Electron 的主进程运行,这里的参数总是
  7. // 主程序代码中 require('electron') 的返回结果。
  8. return app.isPackaged
  9. })
  10. expect(isPackaged).toBe(false)
  11. // Wait for the first BrowserWindow to open
  12. // and return its Page object
  13. const window = await electronApp.firstWindow()
  14. await window.screenshot({ path: 'intro.png' })
  15. // close app
  16. await electronApp.close()
  17. })

然后,使用 npx playwright test 运行 Playwright 测试。 您应该在您的控制台中看到测试通过,并在您的文件系统上看到一个屏幕截图 intro.png

  1. $ npx playwright test
  2. Running 1 test using 1 worker
  3. example.spec.js:4:1 example test (1s)

自动化测试 - 图1info

PlayWright Test 将自动运行与正则表达式 .*(test|spec)\.(js|ts|mjs) 匹配的所有文件。 您可以在 Playwright Test 配置选项 中自定义这个正则表达式。

:::延伸阅读

查看 Playwright完整的 ElectronElectronApplication class API。

:::

使用自定义测试驱动

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

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

testDriver.js

  1. const childProcess = require('node:child_process')
  2. const electronPath = require('electron')
  3. // spawn the process
  4. const env = { /* ... */ }
  5. const stdio = ['inherit', 'inherit', 'inherit', 'ipc']
  6. const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env })
  7. // listen for IPC messages from the app
  8. appProcess.on('message', (msg) => {
  9. // ...
  10. })
  11. // send an IPC message to the app
  12. appProcess.send({ my: 'message' })

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

main.js

  1. // 监听测试套件发送过来的消息
  2. process.on('message', (msg) => {
  3. // ...
  4. })
  5. // 发送一条消息到测试套件
  6. process.send({ my: 'message' })

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

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

testDriver.js

  1. class TestDriver {
  2. constructor ({ path, args, env }) {
  3. this.rpcCalls = []
  4. // 启动子进程
  5. env.APP_TEST_DRIVER = 1 // 让应用知道它应当侦听信息
  6. this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })
  7. // 处理RPC回复
  8. this.process.on('message', (message) => {
  9. // 弹出处理器
  10. const rpcCall = this.rpcCalls[message.msgId]
  11. if (!rpcCall) return
  12. this.rpcCalls[message.msgId] = null
  13. // 拒绝/接受(reject/resolve)
  14. if (message.reject) rpcCall.reject(message.reject)
  15. else rpcCall.resolve(message.resolve)
  16. })
  17. // 等待准备完毕
  18. this.isReady = this.rpc('isReady').catch((err) => {
  19. console.error('Application failed to start', err)
  20. this.stop()
  21. process.exit(1)
  22. })
  23. }
  24. // 简单 RPC 回调
  25. // 可以使用:driver.rpc('method', 1, 2, 3).then(...)
  26. async rpc (cmd, ...args) {
  27. // send rpc request
  28. const msgId = this.rpcCalls.length
  29. this.process.send({ msgId, cmd, args })
  30. return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject }))
  31. }
  32. stop () {
  33. this.process.kill()
  34. }
  35. }
  36. module.exports = { TestDriver }

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

main.js

  1. const METHODS = {
  2. isReady () {
  3. // 进行任何需要的初始化
  4. return true
  5. }
  6. // 在这里定义可做 RPC 调用的方法
  7. }
  8. const onMessage = async ({ msgId, cmd, args }) => {
  9. let method = METHODS[cmd]
  10. if (!method) method = () => new Error('Invalid method: ' + cmd)
  11. try {
  12. const resolve = await method(...args)
  13. process.send({ msgId, resolve })
  14. } catch (err) {
  15. const reject = {
  16. message: err.message,
  17. stack: err.stack,
  18. name: err.name
  19. }
  20. process.send({ msgId, reject })
  21. }
  22. }
  23. if (process.env.APP_TEST_DRIVER) {
  24. process.on('message', onMessage)
  25. }

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

test.js

  1. const test = require('ava')
  2. const electronPath = require('electron')
  3. const { TestDriver } = require('./testDriver')
  4. const app = new TestDriver({
  5. path: electronPath,
  6. args: ['./app'],
  7. env: {
  8. NODE_ENV: 'test'
  9. }
  10. })
  11. test.before(async t => {
  12. await app.isReady
  13. })
  14. test.after.always('cleanup', async t => {
  15. await app.stop()
  16. })

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