2018-04-20 16:39:13 +00:00
# Automated Testing with a Custom Driver
2020-11-02 09:58:14 +00:00
To write automated tests for your Electron app, you will need a way to "drive" your application. [Spectron ](https://electronjs.org/spectron ) is a commonly-used solution which lets you emulate user actions via [WebDriver ](https://webdriver.io/ ). 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.
2018-04-20 16:39:13 +00:00
2019-06-21 21:19:21 +00:00
To create a custom driver, we'll use Node.js' [child_process ](https://nodejs.org/api/child_process.html ) API. The test suite will spawn the Electron process, then establish a simple messaging protocol:
2018-04-20 16:39:13 +00:00
```js
2020-01-13 01:29:46 +00:00
const childProcess = require('child_process')
const electronPath = require('electron')
2018-04-20 16:39:13 +00:00
// spawn the process
2020-07-09 17:18:49 +00:00
const env = { /* ... */ }
const stdio = ['inherit', 'inherit', 'inherit', 'ipc']
const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env })
2018-04-20 16:39:13 +00:00
// listen for IPC messages from the app
appProcess.on('message', (msg) => {
// ...
})
// send an IPC message to the app
2018-09-13 16:10:51 +00:00
appProcess.send({ my: 'message' })
2018-04-20 16:39:13 +00:00
```
2019-06-21 21:19:21 +00:00
From within the Electron app, you can listen for messages and send replies using the Node.js [process ](https://nodejs.org/api/process.html ) API:
2018-04-20 16:39:13 +00:00
```js
// listen for IPC messages from the test suite
process.on('message', (msg) => {
// ...
})
// send an IPC message to the test suite
2018-09-13 16:10:51 +00:00
process.send({ my: 'message' })
2018-04-20 16:39:13 +00:00
```
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:
```js
class TestDriver {
2018-09-13 16:10:51 +00:00
constructor ({ path, args, env }) {
2018-04-20 16:39:13 +00:00
this.rpcCalls = []
// start child process
env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages
2018-09-13 16:10:51 +00:00
this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })
2018-04-20 16:39:13 +00:00
// handle rpc responses
this.process.on('message', (message) => {
// pop the handler
2020-07-09 17:18:49 +00:00
const rpcCall = this.rpcCalls[message.msgId]
2018-04-20 16:39:13 +00:00
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
2020-07-09 17:18:49 +00:00
const msgId = this.rpcCalls.length
2018-09-13 16:10:51 +00:00
this.process.send({ msgId, cmd, args })
return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject }))
2018-04-20 16:39:13 +00:00
}
stop () {
this.process.kill()
}
}
```
In the app, you'd need to write a simple handler for the RPC calls:
```js
2021-08-02 01:57:37 +00:00
const METHODS = {
isReady () {
// do any setup needed
return true
}
// define your RPC-able methods here
2018-04-20 16:39:13 +00:00
}
2021-08-02 01:57:37 +00:00
const onMessage = async ({ msgId, cmd, args }) => {
2020-01-13 01:29:46 +00:00
let method = METHODS[cmd]
2018-04-20 16:39:13 +00:00
if (!method) method = () => new Error('Invalid method: ' + cmd)
try {
2020-07-09 17:18:49 +00:00
const resolve = await method(...args)
2018-09-13 16:10:51 +00:00
process.send({ msgId, resolve })
2018-04-20 16:39:13 +00:00
} catch (err) {
2020-07-09 17:18:49 +00:00
const reject = {
2018-04-20 16:39:13 +00:00
message: err.message,
stack: err.stack,
name: err.name
}
2018-09-13 16:10:51 +00:00
process.send({ msgId, reject })
2018-04-20 16:39:13 +00:00
}
}
2021-08-02 01:57:37 +00:00
if (process.env.APP_TEST_DRIVER) {
process.on('message', onMessage)
2018-04-20 16:39:13 +00:00
}
```
Then, in your test suite, you can use your test-driver as follows:
```js
2020-01-13 01:29:46 +00:00
const test = require('ava')
const electronPath = require('electron')
2018-04-20 16:39:13 +00:00
2020-07-09 17:18:49 +00:00
const app = new TestDriver({
2018-04-20 16:39:13 +00:00
path: electronPath,
args: ['./app'],
env: {
NODE_ENV: 'test'
}
})
test.before(async t => {
await app.isReady
})
test.after.always('cleanup', async t => {
await app.stop()
})
```