-
Notifications
You must be signed in to change notification settings - Fork 147
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
389 additions
and
286 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
docs/latest/tutorial/automated-testing-with-a-custom-driver.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
--- | ||
title: "Automated Testing with a Custom Driver" | ||
description: "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." | ||
slug: automated-testing-with-a-custom-driver | ||
hide_title: false | ||
--- | ||
|
||
# Automated Testing with a Custom Driver | ||
|
||
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. | ||
|
||
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: | ||
|
||
```js | ||
const childProcess = require('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' }) | ||
``` | ||
|
||
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: | ||
|
||
```js | ||
// 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: | ||
|
||
```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() | ||
} | ||
} | ||
``` | ||
|
||
In the app, you'd need to write a simple handler for the RPC calls: | ||
|
||
```js | ||
if (process.env.APP_TEST_DRIVER) { | ||
process.on('message', onMessage) | ||
} | ||
|
||
async function onMessage ({ 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 }) | ||
} | ||
} | ||
|
||
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: | ||
|
||
```js | ||
const test = require('ava') | ||
const electronPath = require('electron') | ||
|
||
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() | ||
}) | ||
``` |
Oops, something went wrong.