Automated UI & functional testing with WebDriverIO

Automated UI & functional testing with WebDriverIO

There is an abundance of E2E and UI automation frameworks in the wild and they’re worth exploring to understand each of their strengths and weaknesses. This way, we can make a judgment on which tools work best in various situations.

One of these tools is WebDriverIO, an independently-written wrapper of the WebDriver APIs for Node.js. I chose to delve in it for the following reasons:

  • Many features out of the box and can be customized/extended
  • Multi-browser support
  • The tech stack for the solution was Node.js and I wanted to stick within the same stack – Easy integration into the code base, easier code reviews, part of the already used stack

In this post, I’ll walk through how I used WebDriverIO to create a robust framework with features I like to have.

I’ve uploaded my starter project to my GitHub account here.


WebDriverIO is extremely easy to setup and you could be running a script within minutes. Their documentation is top-notch for getting started. I will not repeat their docs (go ahead and follow it now), but will discuss ES6 and JUnit report generation setup.

ES6 Support

The benefits of adding ES6 can be found on other sites, but in summary, simplicity, cleaner code, and advanced JavaScript features. We can use Babel to add ES6 support to WebDriverIO.

  1. Install the @babel/core, @babel/preset-env, and @babel/register packages
  2. Add a babel.config.js file in the top level directory
// babel.config.js:

module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        node: 'current'

Finally, in the wdio.conf.js file, add the following to mochaOpts:

require: ['@babel/register'] 

JUnit Report

Next, we want to generate a result file, preferably in XML format, to be consumed by Continuous Development/Integration pipelines so we can easily see results from the pipeline UI as opposed to sifting through the console outputs. Add the following to your wdio.conf.js file:

reporters: [
        outputDir: './test-results',
        outputFileFormat: function (opts) {
          return `results-${opts.cid}.${opts.capabilities.browserName}.xml`;

And that’s it! We can now move on to the extra features.


Screenshots on Failure

To help us with debugging, it’s good to visually see what happened on failure. This is especially useful when running in CI/CD. WebDriverIO comes with many hooks, which can be found in the conf.js file, and in this case we’ll be using the afterTest hook:

afterTest: function(test, context, { error, result, duration, passed, retries }) {
    if (test.passed) {
    // Screenshots on failure
    // Get current test file name
    var filename = encodeURIComponent(test.title.replace(/\s+/g, '-'));
    var timestamp = new Date().toJSON().replace(/:/g, '-');
    var browserName = browser.capabilities.browserName;
    // Build file path
    var filePath = resultsPath + filename + '_' +
      browserName + '_' + timestamp + '.png';
    // Save screenshot

Multiple Run Configurations

You may want to use different services, settings, urls, and more when running tests locally vs in CI/CD. To achieve this, you can use multiple conf.js files and incorporate the deepmerge library to eliminate code duplication.

In my case, I had wdio.conf.main.js file where all common settings across configurations lived. From there, I had a wdio.conf.local.js for running locally, a wdio.conf.docker.js for running in docker, and wdio.conf.bstack.js for running with BrowserStack.

  1. Install the deepmerge package
  2. Setup the main conf file as you like (you don’t have to rename the original one, I just did to make it obvious)
  3. Add any new conf files in the same directory
  4. Use deepmerge in the files created in step 3 to inherit from the main conf
  5. Update the conf files to include what you need
// wdio.conf.local.js

const deepmerge = require('deepmerge');
const wdioConf = require('./wdio.conf.main.js');

exports.config = deepmerge(
    // Using standalone to cover various browsers
    port: 4441,
    services: ['selenium-standalone'],
    baseUrl: '',

    capabilities: [
        acceptInsecureCerts: true, // To handle unsecure certs
        browserName: process.env.BROWSER || 'chrome'
  { clone: false }

Specifying Browser on RUN

Instead of having all browsers run at the same time when writing tests, I like to run one browser and check others individually. To achieve this, I like to pass the browser from the CLI instead of changing the wdio.conf file every time.

To start, use the selenium-standalone service so you don’t have to configure WebDrivers. Then in your wdio.conf file, update your capabilities to the following:

capabilities: [
    acceptInsecureCerts: true, // To handle unsecure certs
    browserName: process.env.BROWSER || 'chrome'

Now you can pass the browser as an environment variable like so:

BROWSER=firefox ./node_modules/.bin/wdio wdio.conf.local.js

Docker Integration

WebDriverIO is able to run with Docker in multiple ways. You can use the Docker service or spin up a selenium-standalone server container.

const deepmerge = require('deepmerge');
const wdioConf = require('./wdio.conf.main.js');

// This is a setup for running tests against a public site.
exports.config = deepmerge(wdioConf.config, {
  hostname: 'localhost',
  baseUrl: "",

  services: ['docker'],
  dockerOptions: {
    image: 'selenium/standalone-chrome',
    healthCheck: 'http://localhost:4444',
    options: {
      p: ['4444:4444'],
      shmSize: '2g'
  capabilities: [{
    acceptInsecureCerts: true,
    browserName: 'chrome'
}, { clone: false });

To use a selenium-standalone container:

      - app
    image: selenium/standalone-chrome
      - /dev/shm:/dev/shm
      - "4444:4444"

Then set the hostname variable in the conf file to be ‘selenium’.

WebDriverIO is a fast, reliable, and feature-rich automation tool. I highly suggest you give it a try for any kind of application. As I mentioned earlier, you can find my starter project here to hit the road running.