自動化測試
自動化測試是一種有效驗證應用程式程式碼是否按預期運作的方式。雖然 Electron 並不積極維護自己的測試解決方案,但本指南將介紹幾種在您的 Electron 應用程式上執行端對端自動化測試的方法。
使用 WebDriver 介面
來自 ChromeDriver - 適用於 Chrome 的 WebDriver
WebDriver 是一個開源工具,用於自動化測試許多瀏覽器上的 Web 應用程式。它提供導航到網頁、使用者輸入、JavaScript 執行等功能。ChromeDriver 是一個獨立的伺服器,它實現了 Chromium 的 WebDriver 線路協議。它由 Chromium 和 WebDriver 團隊的成員開發。
您可以使用 WebDriver 設定測試的幾種方式。
使用 WebdriverIO
WebdriverIO (WDIO) 是一個測試自動化框架,提供用於使用 WebDriver 進行測試的 Node.js 套件。它的生態系統還包括各種外掛程式(例如,報告器和服務),可以幫助您建立測試設定。
如果您已經有現有的 WebdriverIO 設定,建議您更新您的依賴項,並根據文件中概述的方式驗證您現有的設定。
安裝測試執行器
如果您的專案中尚未使用 WebdriverIO,您可以在專案根目錄中執行啟動工具組來新增它
- 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('https://127.0.0.1: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|spec)\.(js|ts|mjs)
正規表達式的檔案。您可以在Playwright 測試設定選項中自訂此匹配項。它也可以與 TypeScript 開箱即用地配合使用。
查看 Playwright 的文件,以取得完整的 Electron 和 ElectronApplication 類別 API。
使用自訂測試驅動程式
您也可以使用 Node.js 內建的 IPC-over-STDIO 撰寫自己的自訂驅動程式。自訂測試驅動程式需要您撰寫額外的應用程式碼,但其開銷較低,並且可以讓您向測試套件公開自訂方法。
若要建立自訂驅動程式,我們將使用 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()
})