メインコンテンツにスキップ

自動テスト

テストの自動化は、アプリケーションコードが意図したとおりに動作することを検証する効率的な方法です。Electronは独自のテストソリューションを積極的にメンテナンスしていませんが、このガイドではElectronアプリでエンドツーエンドの自動テストを実行するいくつかの方法について説明します。

WebDriverインターフェースの使用

ChromeDriver - WebDriver for Chromeより

WebDriverは、多くのブラウザでWebアプリの自動テストを行うためのオープンソースツールです。Webページへの移動、ユーザー入力、JavaScriptの実行など、さまざまな機能を提供します。ChromeDriverは、ChromiumのWebDriverのワイヤプロトコルを実装するスタンドアロンサーバーです。ChromiumとWebDriverチームのメンバーによって開発されています。

WebDriverを使用してテストを設定するには、いくつかの方法があります。

WebdriverIOを使用する

WebdriverIO (WDIO) は、WebDriverを使用したテストのためのNode.jsパッケージを提供するテスト自動化フレームワークです。そのエコシステムには、テストの設定を支援するさまざまなプラグイン(例:レポーターとサービス)も含まれています。

既存のWebdriverIO設定がすでにある場合は、依存関係を更新し、既存の設定をドキュメントに記載されている方法で検証することをお勧めします。

テストランナーをインストールする

プロジェクトでまだWebdriverIOを使用していない場合は、プロジェクトのルートディレクトリでスターターツールキットを実行することで追加できます。

npm init wdio@latest ./

これにより、適切な設定を行うための設定ウィザードが起動し、必要なすべてのパッケージがインストールされ、`wdio.conf.js`設定ファイルが生成されます。「どのような種類のテストを行いたいですか?」という最初の質問の1つで、「デスクトップテスト-Electronアプリケーションの」を選択してください。

WDIOをElectronアプリに接続する

設定ウィザードを実行した後、`wdio.conf.js`には、おおよそ次の内容が含まれているはずです。

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は、アプリケーションの起動とシャットダウンを支援します。

その他のドキュメント

Electron APIのモッキングに関するその他のドキュメントやその他の役立つリソースについては、WebdriverIOの公式ドキュメントを参照してください。

Seleniumを使用する

Seleniumは、多くの言語でWebDriver APIへのバインディングを公開するWeb自動化フレームワークです。Node.jsバインディングは、NPMの`selenium-webdriver`パッケージで入手できます。

ChromeDriverサーバーを実行する

ElectronでSeleniumを使用するには、`electron-chromedriver`バイナリをダウンロードして実行する必要があります。

npm install --save-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 install --save-dev selenium-webdriver

Electronでの`selenium-webdriver`の使い方は、通常のWebサイトと同じですが、ChromeDriverの接続方法とElectronアプリのバイナリの場所を手動で指定する必要がある点が異なります。

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の使用

Microsoft Playwrightは、PuppeteerヘッドレスNode.js APIと同様に、ブラウザ固有のリモートデバッグプロトコルを使用して構築されたエンドツーエンドのテストフレームワークですが、エンドツーエンドのテスト向けに調整されています。 Playwrightは、ElectronのChrome DevTools Protocol(CDP)のサポートを通じて、実験的なElectronサポートを提供しています。

依存関係をインストールする

Playwrightは、 preferred Node.jsパッケージマネージャーを通じてインストールできます。エンドツーエンドテスト用に構築された独自のテストランナーが付属しています。

npm install --save-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`テストファイルを作成しましょう。

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 Testを実行します。コンソールでテストに合格し、ファイルシステムに`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の組み込みIPC-over-STDIOを使用して、独自のカスタムドライバーを作成することも可能です。カスタムテストドライバーでは、追加のアプリコードを記述する必要がありますが、オーバーヘッドが少なく、テストスイートにカスタムメソッドを公開できます。

カスタムドライバーを作成するには、Node.jsの`child_process` APIを使用します。テストスイートはElectronプロセスを生成し、単純なメッセージングプロトコルを確立します。

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を使用してメッセージをリッスンし、返信を送信できます。

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アプリと通信できます。

便宜上、より高レベルの関数を備えたドライバオブジェクトに`appProcess`をラップすることができます。その方法の例を次に示します。`TestDriver`クラスを作成することから始めましょう。

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 呼び出しを受信するためのシンプルなハンドラを記述できます。

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 など、他の一般的な選択肢も同様に機能します。

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