自動テスト
テスト自動化は、アプリケーションコードの意図した動作を検証する効率的方法です。 Electron は独自のテストソリューションを積極的には提供していませんが、このガイドでは Electron アプリでエンドツーエンドの自動テストを実行する方法をいくつか説明します。
WebDriver インターフェースを使用する
ChromeDriver - クローム向けのWebDriver:
WebDriverは、ブラウザを横断的なテストの自動化を実現するためのオープンソースツールです。 このドライバはウェブページの遷移、インプット項目への入力、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
設定ファイルを生成します。 "What type of testing would you like to do?" という最初の質問では、"Desktop Testing - of Electron Applications" を必ず選択してください。
Electron アプリを WDIO に接続する
設定ウィザードの実行後、wdio.conf.js
にはおおよそ以下の内容が含まれているはずです。
export const config = {
// ...
services: ['electron'],
capabilities: [{
browserName: 'electron',
'wdio:electronServiceOptions': {
// WebdriverIO は、Electron Forge または electron-builder を
// 使用している場合、自動で以下のようにバンドルされている
// アプリケーションを検知できます。
// 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 () => {
// カスタム引数は WDIO の起動前に設定する必要があるため、wdio.conf.js ファイルにて設定します。
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 は、多くの言語で 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()
// "9515" は ChromeDriver が開けたポートです。
.usingServer('http://localhost:9515')
.withCapabilities({
'goog:chromeOptions': {
// ここに Electron バイナリへのパスを入れます。
binary: '/Path-to-Your-App.app/Contents/MacOS/Electron'
}
})
.forBrowser('chrome') // 注: selenium-webdriver <= 3.6.0 では .forBrowser('electron') を使用してください
.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 デベロッパー ツール プロトコル (CDP) を介して、Electron を実験的にサポートしています。
依存関係をインストールする
Playwright はお好みの Node.js パッケージマネージャでインストールできます。 これには E2E テスト用に構築された独自の テストランナー も付属しています。
- 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'] })
// アプリを閉じます
await electronApp.close()
})
その後、Playwrite の ElectronApp
クラスのインスタンスにアクセスします。 これは、メインプロセスのモジュールにアクセスできる強力なクラスです。
const { test, _electron: electron } = require('playwright')
test('get isPackaged', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// これは Electron のメインプロセスで実行され、この引数は常に
// メインのアプリスクリプトでの require('electron') の戻り値です。
return app.isPackaged
})
console.log(isPackaged) // false (なぜなら開発モードであるから)
// アプリを閉じます
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' })
// アプリを閉じます
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 }) => {
// これは Electron のメインプロセスで実行され、この引数は常に
// メインのアプリスクリプトでの require('electron') の戻り値です。
return app.isPackaged
});
expect(isPackaged).toBe(false);
// 最初の BrowserWindow が開かれるのを待機し、
// その Page オブジェクトが返されます
const window = await electronApp.firstWindow()
await window.screenshot({ path: 'intro.png' })
// アプリを閉じます
await electronApp.close()
});
そして、Playwright テストを npx playwright test
で実行します。 コンソールにテストの通過が表示され、ファイルシステム上に 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 Test の構成オプション にてカスタマイズできます。 TypeScript でも 準備いらずで動作します。
Electron および ElectronApplication クラスの API については、Playwright のドキュメントをご参照ください。
カスタムテストドライバを使用する
また、Node.js 組み込みの標準入出力を介したプロセス間通信を利用して、独自のカスタムドライバーも書けます。 カスタムテストドライバは、アプリのコードを追加で書く必要がありますが、オーバーヘッドが少なく、カスタムメソッドをテストスイートに公開できます。
カスタムドライバを作成するには、Node.js の child_process
API を使用します。 テストスイートは、以下のように Electron プロセスを spawn してから、簡単なメッセージングプロトコルを確立します。
const childProcess = require('node:child_process')
const electronPath = require('electron')
// プロセスを生成します
const env = { /* ... */ }
const stdio = ['inherit', 'inherit', 'inherit', 'ipc']
const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env })
// アプリからの IPC メッセージをリッスンします
appProcess.on('message', (msg) => {
// ...
})
// アプリへ IPC メッセージを送る
appProcess.send({ my: 'message' })
Electron アプリケーション内からは、Node.js の process
API を使用してメッセージをリッスンして返信を送信できます。
// テストスイートからのメッセージをリッスンする
process.on('message', (msg) => {
// ...
})
// テストスイートへメッセージを送る
process.send({ my: 'message' })
これで、appProcess
オブジェクトを使用してテストスイートから Electron アプリケーションに通信できます。
利便性のために、より高度な機能を提供するドライバオブジェクトで appProcess
をラップすることをお勧めします。 以下は、これを実現したサンプルです。 以下の TestDriver
クラスの作成から始めましょう。
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
// 拒否/解決
if (message.reject) rpcCall.reject(message.reject)
else rpcCall.resolve(message.resolve)
})
// ready を待つ
this.isReady = this.rpc('isReady').catch((err) => {
console.error('Application failed to start', err)
this.stop()
process.exit(1)
})
}
// 簡単な RPC 呼び出し
// to use: driver.rpc('method', 1, 2, 3).then(...)
async rpc (cmd, ...args) {
// RPC リクエストを送ります
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 () {
// ここで必要なセットアップをします
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 など他の一般的な選択肢も同様に機能します。
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()
})