Browse Source

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
Paul Frazee 7 years ago
parent
commit
94236bf4eb
2 changed files with 137 additions and 1 deletions
  1. 2 1
      docs/README.md
  2. 135 0
      docs/tutorial/automated-testing-with-a-custom-driver.md

+ 2 - 1
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)
+See [development/README.md](development/README.md)

+ 135 - 0
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()
+})
+```