To write automated tests for your Electron app, you will need a way to "drive" your application. Spectron is a commonly-used solution which lets you emulate user actions via WebDriver. However, it's also possible to write your own custom driver using node's builtin IPC-over-STDIO. The benefit of a custom driver is that it tends to require less overhead than Spectron, and lets you expose custom methods to your test suite.
To create a custom driver, we'll use nodejs' child_process API. The test suite will spawn the Electron process, then establish a simple messaging protocol:
var childProcess = require('child_process') var electronPath = require('electron') // spawn the process var env = { /* ... */ } var stdio = ['inherit', 'inherit', 'inherit', 'ipc'] var 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'})
From within the Electron app, you can listen for messages and send replies using the nodejs process API:
// listen for IPC messages from the test suite process.on('message', (msg) => { // ... }) // send an IPC message to the test suite process.send({my: 'message'})
We can now communicate from the test suite to the Electron app using the appProcess
object.
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:
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 var 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 var msgId = this.rpcCalls.length this.process.send({msgId, cmd, args}) return new Promise((resolve, reject) => this.rpcCalls.push({resolve, reject})) } stop () { this.process.kill() } }
In the app, you'd need to write a simple handler for the RPC calls:
if (process.env.APP_TEST_DRIVER) { process.on('message', onMessage) } async function onMessage ({msgId, cmd, args}) { var method = METHODS[cmd] if (!method) method = () => new Error('Invalid method: ' + cmd) try { var resolve = await method(...args) process.send({msgId, resolve}) } catch (err) { var reject = { message: err.message, stack: err.stack, name: err.name } process.send({msgId, reject}) } } const METHODS = { isReady () { // do any setup needed return true } // define your RPC-able methods here }
Then, in your test suite, you can use your test-driver as follows:
var test = require('ava') var electronPath = require('electron') var 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() })
© 2013–2018 GitHub Inc.
Licensed under the MIT license.
https://electronjs.org/docs/tutorial/automated-testing-with-a-custom-driver