Automated Testing

Test automation is an efficient way of validating that your application code works as intended. While Electron doesn’t actively maintain its own testing solution, this guide will go over a couple ways you can run end-to-end automated tests on your Electron app.

Using the WebDriver interface

From ChromeDriver - WebDriver for Chrome:

WebDriver is an open source tool for automated testing of web apps across many browsers. It provides capabilities for navigating to web pages, user input, JavaScript execution, and more. ChromeDriver is a standalone server which implements WebDriver’s wire protocol for Chromium. It is being developed by members of the Chromium and WebDriver teams.

There are a few ways that you can set up testing using WebDriver.

With WebdriverIO

WebdriverIO (WDIO) is a test automation framework that provides a Node.js package for testing with WebDriver. Its ecosystem also includes various plugins (e.g. reporter and services) that can help you put together your test setup.

Install the testrunner

First you need to run the WebdriverIO starter toolkit in your project root directory:

  • npm
  • Yarn
  1. npx wdio . --yes
  1. npx wdio . --yes

This installs all necessary packages for you and generates a wdio.conf.js configuration file.

Connect WDIO to your Electron app

Update the capabilities in your configuration file to point to your Electron app binary:

wdio.conf.js

  1. export.config = {
  2. // ...
  3. capabilities: [{
  4. browserName: 'chrome',
  5. 'goog:chromeOptions': {
  6. binary: '/path/to/your/electron/binary', // Path to your Electron binary.
  7. args: [/* cli arguments */] // Optional, perhaps 'app=' + /path/to/your/app/
  8. }
  9. }]
  10. // ...
  11. }

Run your tests

To run your tests:

  1. $ npx wdio run wdio.conf.js

With Selenium

Selenium is a web automation framework that exposes bindings to WebDriver APIs in many languages. Their Node.js bindings are available under the selenium-webdriver package on NPM.

Run a ChromeDriver server

In order to use Selenium with Electron, you need to download the electron-chromedriver binary, and run it:

  • npm
  • Yarn
  1. npm install --save-dev electron-chromedriver
  2. ./node_modules/.bin/chromedriver
  3. Starting ChromeDriver (v2.10.291558) on port 9515
  4. Only local connections are allowed.
  1. yarn add --dev electron-chromedriver
  2. ./node_modules/.bin/chromedriver
  3. Starting ChromeDriver (v2.10.291558) on port 9515
  4. Only local connections are allowed.

Remember the port number 9515, which will be used later.

Connect Selenium to ChromeDriver

Next, install Selenium into your project:

  • npm
  • Yarn
  1. npm install --save-dev selenium-webdriver
  1. yarn add --dev selenium-webdriver

Usage of selenium-webdriver with Electron is the same as with normal websites, except that you have to manually specify how to connect ChromeDriver and where to find the binary of your Electron app:

test.js

  1. const webdriver = require('selenium-webdriver')
  2. const driver = new webdriver.Builder()
  3. // The "9515" is the port opened by ChromeDriver.
  4. .usingServer('http://localhost:9515')
  5. .withCapabilities({
  6. 'goog:chromeOptions': {
  7. // Here is the path to your Electron binary.
  8. binary: '/Path-to-Your-App.app/Contents/MacOS/Electron'
  9. }
  10. })
  11. .forBrowser('chrome') // note: use .forBrowser('electron') for selenium-webdriver <= 3.6.0
  12. .build()
  13. driver.get('http://www.google.com')
  14. driver.findElement(webdriver.By.name('q')).sendKeys('webdriver')
  15. driver.findElement(webdriver.By.name('btnG')).click()
  16. driver.wait(() => {
  17. return driver.getTitle().then((title) => {
  18. return title === 'webdriver - Google Search'
  19. })
  20. }, 1000)
  21. driver.quit()

Using a custom test driver

It’s also possible to write your own custom driver using Node.js’ built-in IPC-over-STDIO. Custom test drivers require you to write additional app code, but have lower overhead and let you expose custom methods to your test suite.

To create a custom driver, we’ll use Node.js’ child_process API. The test suite will spawn the Electron process, then establish a simple messaging protocol:

testDriver.js

  1. const childProcess = require('child_process')
  2. const electronPath = require('electron')
  3. // spawn the process
  4. const env = { /* ... */ }
  5. const stdio = ['inherit', 'inherit', 'inherit', 'ipc']
  6. const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env })
  7. // listen for IPC messages from the app
  8. appProcess.on('message', (msg) => {
  9. // ...
  10. })
  11. // send an IPC message to the app
  12. appProcess.send({ my: 'message' })

From within the Electron app, you can listen for messages and send replies using the Node.js process API:

main.js

  1. // listen for messages from the test suite
  2. process.on('message', (msg) => {
  3. // ...
  4. })
  5. // send a message to the test suite
  6. 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. Let’s start by creating a TestDriver class:

testDriver.js

  1. class TestDriver {
  2. constructor ({ path, args, env }) {
  3. this.rpcCalls = []
  4. // start child process
  5. env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages
  6. this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })
  7. // handle rpc responses
  8. this.process.on('message', (message) => {
  9. // pop the handler
  10. const rpcCall = this.rpcCalls[message.msgId]
  11. if (!rpcCall) return
  12. this.rpcCalls[message.msgId] = null
  13. // reject/resolve
  14. if (message.reject) rpcCall.reject(message.reject)
  15. else rpcCall.resolve(message.resolve)
  16. })
  17. // wait for ready
  18. this.isReady = this.rpc('isReady').catch((err) => {
  19. console.error('Application failed to start', err)
  20. this.stop()
  21. process.exit(1)
  22. })
  23. }
  24. // simple RPC call
  25. // to use: driver.rpc('method', 1, 2, 3).then(...)
  26. async rpc (cmd, ...args) {
  27. // send rpc request
  28. const msgId = this.rpcCalls.length
  29. this.process.send({ msgId, cmd, args })
  30. return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject }))
  31. }
  32. stop () {
  33. this.process.kill()
  34. }
  35. }
  36. module.exports = { TestDriver };

In your app code, can then write a simple handler to receive RPC calls:

main.js

  1. const METHODS = {
  2. isReady () {
  3. // do any setup needed
  4. return true
  5. }
  6. // define your RPC-able methods here
  7. }
  8. const onMessage = async ({ msgId, cmd, args }) => {
  9. let method = METHODS[cmd]
  10. if (!method) method = () => new Error('Invalid method: ' + cmd)
  11. try {
  12. const resolve = await method(...args)
  13. process.send({ msgId, resolve })
  14. } catch (err) {
  15. const reject = {
  16. message: err.message,
  17. stack: err.stack,
  18. name: err.name
  19. }
  20. process.send({ msgId, reject })
  21. }
  22. }
  23. if (process.env.APP_TEST_DRIVER) {
  24. process.on('message', onMessage)
  25. }

Then, in your test suite, you can use your TestDriver class with the test automation framework of your choosing. The following example uses ava, but other popular choices like Jest or Mocha would work as well:

test.js

  1. const test = require('ava')
  2. const electronPath = require('electron')
  3. const { TestDriver } = require('./testDriver')
  4. const app = new TestDriver({
  5. path: electronPath,
  6. args: ['./app'],
  7. env: {
  8. NODE_ENV: 'test'
  9. }
  10. })
  11. test.before(async t => {
  12. await app.isReady
  13. })
  14. test.after.always('cleanup', async t => {
  15. await app.stop()
  16. })