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
This commit is contained in:
		
					parent
					
						
							
								f9ee24f8f0
							
						
					
				
			
			
				commit
				
					
						94236bf4eb
					
				
			
		
					 2 changed files with 137 additions and 1 deletions
				
			
		| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										135
									
								
								docs/tutorial/automated-testing-with-a-custom-driver.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								docs/tutorial/automated-testing-with-a-custom-driver.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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()
 | 
			
		||||
})
 | 
			
		||||
```
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue