From 94236bf4ebb32b845bcac6e8bf0fe321e9fd83a2 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 20 Apr 2018 11:39:13 -0500 Subject: [PATCH] Create automated-testing-with-a-custom-driver.md (#12446) * Create automated-testing-with-a-custom-driver.md * Update automated-testing-with-a-custom-driver.md * Add 'Automated Testing with Custom Driver' to ToC * Update automated-testing-with-a-custom-driver.md --- docs/README.md | 3 +- .../automated-testing-with-a-custom-driver.md | 135 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 docs/tutorial/automated-testing-with-a-custom-driver.md diff --git a/docs/README.md b/docs/README.md index 82744c13e82..d9623d8d425 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,6 +59,7 @@ an issue: * [Using Selenium and WebDriver](tutorial/using-selenium-and-webdriver.md) * [Testing on Headless CI Systems (Travis, Jenkins)](tutorial/testing-on-headless-ci.md) * [DevTools Extension](tutorial/devtools-extension.md) + * [Automated Testing with a Custom Driver](tutorial/automated-testing-with-a-custom-driver.md) * [Application Distribution](tutorial/application-distribution.md) * [Supported Platforms](tutorial/supported-platforms.md) * [Mac App Store](tutorial/mac-app-store-submission-guide.md) @@ -152,4 +153,4 @@ These individual tutorials expand on topics discussed in the guide above. ## Development -See [development/README.md](development/README.md) \ No newline at end of file +See [development/README.md](development/README.md) diff --git a/docs/tutorial/automated-testing-with-a-custom-driver.md b/docs/tutorial/automated-testing-with-a-custom-driver.md new file mode 100644 index 00000000000..4856ac14593 --- /dev/null +++ b/docs/tutorial/automated-testing-with-a-custom-driver.md @@ -0,0 +1,135 @@ +# 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](http://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 nodejs' [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 +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](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 + 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: + +```js +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: + +```js +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() +}) +```