Author avatar

Chris Parker

How to Test React Apps using Selenium WebDriver

Chris Parker

  • Apr 21, 2020
  • 17 Min read
  • 1,501 Views
  • Apr 21, 2020
  • 17 Min read
  • 1,501 Views
Web Development
React

Introduction

Selenium is one of the best frameworks for testing web apps. You can use it to write end-to-end tests that inspect the performance of web apps on actual browsers or, in other words, automate browsers for testing purposes.

But reliable end-to-end tests are hard to write. Many developers report constant failures without apparent reasons.

In this guide, you'll learn how to use Selenium Web Driver in Node.js to construct and design practical tests. The sample test in this guide targets a plain autocomplete form because you need a real browser to test such components. Continue reading to get a better insight into writing practical e2e tests.

What is the Selenium WebDriver?

Selenium incorporates a server that basically launches a browser and then listens for incoming connections. Once a client tries to communicate with the server and a connection is established, the server receives commands from the client and relays them to the browser to be executed. The Selenium WebDriver serves as a client for this server, as it facilitates dispatching commands from a programming language. Node.js is used in the example below.

1
2
Node.js <=> WebDriver <=> Selenium Server <=> Firefox/Chrome/IE/Safari
              Client         Server               Browser
text

For more information on the WebDriver, take a look at its documentation.

You can set up the WebDriver using Node.js as follows:

1
2
3
4
const webdriver = require("selenium-webdriver");
const driver = new webdriver.Builder().forBrowser("firefox").build();
// Get the browser to open a new page
driver.navigate().to("http://path.to.test.app/");
javascript

Note that the example above uses Node.js v5 as it natively supports many ES6 features.

Promises and ControlFlow

All WebDriver methods are asynchronous; in other words, they return Promises. Whether you plan to use a CSS selector to locate an element or you wish to alter its value, both operations are async. You can use then() to chain the Promises returned by them as follows:

1
2
3
4
5
6
7
8
const By = webdriver.By; // useful Locator utility to describe a query for a WebElement
// open a page, find autocomplete input by CSS selector, then get its value
driver
  .navigate()
  .to("http://path.to.test.app/")
  .then(() => driver.findElement(By.css(".autocomplete")))
  .then(element => element.getAttribute("value"))
  .then(value => console.log(value));
javascript

As you see, the command driver.findElement() returns a WebElement Promise. You can use this Promise as an interface to handle and probe the DOM elements in Selenium. Moreover, WebElements promises have immediate access to web element methods, which means you can combine the second and third line in the above example together as follows:

1
2
3
4
5
driver
  .navigate()
  .to("http://path.to.test.app/")
  .then(() => driver.findElement(By.css(".autocomplete")).getAttribute("value"))
  .then(value => console.log(value));
javascript

However, Promises become bothersome once you try to chain several actions together:

1
2
3
4
5
6
const until = webdriver.until; // useful utility to wait for something to happen
// fill the input with 'John', wait for the suggestion list to appear, then click on the first suggestion ('John Doe')
driver.navigate().to('http://path.to.test.app/')
    .then(() => driver.findElement(By.css('.autocomplete')).sendKeys('John'))
    .then(() => driver.wait(until.elementLocated(By.css('.suggestion'))))
    .then(() => driver.findElement(By.css('.suggestion')).click()))
javascript

Remember that WebDriver promises are not regular Promises. Every time you summon an async WebDriver task and leave it unresolved, the WebDriver pushes it to a global queue. After you resolve a Promise, or following the subsequent iteration of the JavaScript event loop, JavaScript executes all queued tasks per the order of scheduling just like they were typical synchronous operations. This is referred to as ControlFlow:

1
2
3
4
5
6
7
8
9
10
// add promise to our task queue
driver.navigate().to('http://path.to.test.app/');
// add a second promise to the task queue, as if using then()
driver.findElement(By.css('.autocomplete')).sendKeys('John');
// add third promise to our task queue, as if using then()
driver.wait(until.elementLocated(By.css('.suggestion')));
// add fourth promise to our task queue, as if using then()
driver.findElement(By.css('.suggestion')).click()
// execute all promises in our task queue
    .then(() => ...)
javascript

In this case, your code readability improves tremendously, and you require a Promise for the last element only. You may find that hard to believe as there exist four async operations; each one of them returns a promise, but they are executed consecutively. As you see, Selenium tests are much easier to write using ControlFlow.

Promises in Tests

The example above returns a Promise. That means that if you wish to use it in testing, you must employ asynchronous tests. In Mocha, for example, you need to call the done function parameter so that your test runner knows whether the test finished or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//in e2e/tests/autocomplete.js
const webdriver = require("selenium-webdriver");

const driver = new webdriver.Builder().forBrowser("firefox").build();

describe("login form", () => {
  // e2e tests are too slow for default Mocha timeout
  this.timeout(10000);

  before(function(done) {
    driver
      .navigate()
      .to("http://path.to.test.app/")
      .then(() => done());
  });

  it("autocompletes the name field", function(done) {
    driver.findElement(By.css(".autocomplete")).sendKeys("John");
    driver.wait(until.elementLocated(By.css(".suggestion")));
    driver
      .findElement(By.css(".suggestion"))
      .click()
      .then(() => done());
  });

  after(function(done) {
    driver.quit().then(() => done());
  });
});
javascript

Remember to close the test browser window by exiting the driver once your test finishes.

Mocha accepts Promise as a return value, which offers a better alternative for the above approach. Once a test returns a promise, Mocha handles it as if the operation is asynchronous and pauses expecting the Promise to resolve before it moves on to the next test:

1
2
3
4
5
6
7
8
9
before(() => driver.navigate().to("http://path.to.test.app/"));

it("autocompletes the name field", function() {
  driver.findElement(By.css(".autocomplete")).sendKeys("John");
  driver.wait(until.elementLocated(By.css(".suggestion")));
  return driver.findElement(By.css(".suggestion")).click();
});

after(() => driver.quit());
javascript

Remember that Mocha does not regard the test as asynchronous if you neglect to return the last Promise. Instead, Mocha proceeds to execute the subsequent test without waiting for the current one to finish. That is why it is essential to return a Promise at the end of every async test in Mocha.

Async Assertions

You have seen how, using ControlFlow and Mocha, your test is considerably more readable without the usual Promise complexities. But don't breathe easy just yet--you still have to address assertions.

1
2
3
4
5
6
7
8
9
10
11
const chai = require("chai");
const expect = chai.expect;
it("autocompletes the name field", function() {
  driver.findElement(By.css(".autocomplete")).sendKeys("John");
  driver.wait(until.elementLocated(By.css(".suggestion")));
  driver.findElement(By.css(".suggestion")).click();
  return driver
    .findElement(By.css(".autocomplete"))
    .getAttribute("value")
    .then(inputValue => expect(inputValue).to.equal("Jane Doe"));
});
javascript

The Promise chain is back, as you see. A smooth bypass for this challenge is to use an assertion library that accepts Promises, like chai-as-promised. Now you can use expect() with the keyword eventually:

1
2
3
4
5
6
7
8
9
10
11
12
const chai = require("chai");
const expect = chai.expect;
const chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
it("autocompletes the name field", function() {
  driver.findElement(By.css(".autocomplete")).sendKeys("Jane");
  driver.wait(until.elementLocated(By.css(".suggestion")));
  driver.findElement(By.css(".suggestion")).click();
  return expect(
    driver.findElement(By.css(".autocomplete")).getAttribute("value")
  ).to.eventually.equal("Jane Doe");
});
javascript

This approach allows for a single assertion only since the test needs to return a Promise. If you wish to employ multiple assertions, make sure to blend them into a single Promise:

1
2
3
4
5
6
7
8
9
it('autocompletes the name field', function() {
    driver.findElement(By.css('.autocomplete')).sendKeys('Jane');
    driver.wait(until.elementLocated(By.css('.suggestion')))
    driver.findElement(By.css('.suggestion')).click();
    return Promise.all([
        expect(driver.findElement(By.css('.autocomplete')).getAttribute('value')).to.eventually.equal('Jane Doe'),
        expect(driver.isElementPresent(By.css('.suggestion')).to.eventually.be.false,
    ]);
});
javascript

If you do not wish to sacrifice readability, you have another option: using co-mocha. Co-mocha supports using generators, so you can drop the .eventually keyword and yield WebDriver Promises should the need arise. Furthermore, you do not necessarily need to return a Promise now that the generator enables blocking commands:

1
2
3
4
5
6
7
it('autocompletes the name field', function*() {
    driver.findElement(By.css('.autocomplete')).sendKeys('Jane');
    driver.wait(until.elementLocated(By.css('.suggestion')))
    driver.findElement(By.css('.suggestion')).click();
    expect(yield driver.findElement(By.css('.autocomplete')).getAttribute('value')).to.equal('Jane Doe');
    expect(yield driver.isElementPresent(By.css('.suggestion')).to.be.false;
});
javascript

Use the following CLI command to include co-mocha in your Mocha test suite:

1
./node_modules/.bin/mocha --require co-mocha "./e2e/tests/*.js"

You will find this approach to be smooth and effective, and it also foretells the ultimate approach for handling async operations: async/await (ES7).

Wait Often

You may notice that Selenium tests randomly fail on a regular basis. Yet, driver.wait(() => {}) is a valuable alley to aid your troubleshoot in the event of failures. driver.wait(() => {}) retains the test browser window and enables you to use the Firefox Developer Tools to investigate the app

One of the prominent reasons for these recurring failures is the time that the browser requires to perform actions such as handling JS on the client side. The assertions may arrive ahead of time, and you need to make sure the browser has finished working before you make an assertion.

1
2
3
4
5
6
7
8
9
10
it('autocompletes the name field', function*() {
    driver.findElement(By.css('.autocomplete')).sendKeys('Jane');
    driver.wait(until.elementLocated(By.css('.suggestion')))
    driver.findElement(By.css('.suggestion')).click();
    // wait until suggestion disappears
    driver.wait(until.elementIsNotVisible(By.css('.suggestion')));
    // only then check the value
    expect(yield driver.findElement(By.css('.autocomplete')).getAttribute('value')).to.equal('Jane Doe');
    expect(yield driver.isElementPresent(By.css('.suggestion')).to.be.false;
});
javascript

Such bugs pop up regularly when you write e2e tests, which is why it is essential to use wait() before you make an assertion.

Page Objects

Once you grow accustomed to writing tests, you may notice repetition in code segments within the tests. e2e tests often correlate with plenty of HTML code. When you modify the HTML code, the majority of your e2e tests may fail, and resolving them is both time and effort consuming.

A countermeasure for this is using page objects, which model the areas and components of your app's UI as objects for e2e testing.

No need to import any special libraries for this, a mere JavaScript object does the trick:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// in e2e/pages/home.js
const webdriver = require('selenium-webdriver');
const By = webdriver.By;
const until = webdriver.until;

module.exports = function(driver) {
    const elements = {
        nameInput: By.css('.autocomplete'),
        nameSuggestion: By.css('.suggestion'),
        submitButton: By.css('.submit'),
    };
    return {
        url:  'http://localhost:8081/frontend#/',
        elements: elements,
        waitUntilVisible: function() {
            return driver.wait(until.elementLocated(elements.nameInput));
        },
        navigate: function() {
            driver.navigate().to(this.url);
            return this.waitUntilVisible();
        },
        enterName: function(value) {
            driver.findElement(elements.nameInput).sendKeys(value);
            driver.wait(until.elementLocated(elements.nameSuggestionEl));
            driver.findElement(elements.nameSuggestionEl).click();
            return driver.wait(until.elementIsNotVisible(By.css(elements.nameSuggestionEl)));
        },
        getName: function() {
            return driver.findElement(elements.nameInput).getAttribute('value')
        }
        submit: function() {
            return driver.findElement(elements.submitButton).click();
        },
    };
};
javascript

You can use a page object in e2e tests as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
// in e2e/tests/autocomplete.js
const webdriver = require("selenium-webdriver");

const driver = new webdriver.Builder().forBrowser("firefox").build();

const homePage = require("../pages/home")(driver);

before(() => homePage.navigate());

it("autocompletes the name field", function*() {
  homePage.enterName("Jane");
  expect(yield homePage.getName()).to.equal("Jane Doe");
});
javascript

This way, your code is readable and the page objects smooth the way for HTML manipulation.

Note that page objects do not include assertions, so you can continue using promises. Make sure to use generators along with yield in test files only. You can arrange your page objects either by page or by component in order to write code that is maintained easily.

1
2
3
4
5
6
7
const homePage = require("../pages/home")(driver);
const resultPage = require("../pages/result")(driver);
it("prefills the name form in the result page", function*() {
  homePage.enterName("Jane");
  homePage.submit();
  expect(yield resultPage.getName()).to.equal("Jane Doe");
});
javascript

You may notice that these e2e tests are very similar to behavior-driven development (BDD) tests. But keep in mind that BDD assertions need further action, which is the translation from gherkin to e2e tests. This usually poses high costs in terms of time and expenses, so it is better to stick to e2e unless you can not avoid it.

Alternatives to Raw Selenium WebDriver

If you decide that the Selenium WebDriver API is too lengthy for your liking, you can use an alternative such as Nightwatch.js, webdriver.io, and Chimp.io. These libraries work best on top of WebDriver API.

In the event of a test break, the best course of action is to isolate the least number of components involved. Nightwatch.js, for example, utilizes a patched version of Mocha to address asynchronous assertions, which is both complicated and counterproductive.

If your goal is simply a smarter syntax for frequent operations such as value or content lookup within an input or a text element, you can write a specified utility function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// in e2e/utils/driverUtils.js
const until = require("selenium-webdriver").until;

module.exports = driver => ({
  waitForElementVisible: function(selector) {
    return driver.wait(until.elementLocated(selector));
  },
  getText: function(selector) {
    return driver.findElement(selector).getText();
  },
  getValue: function(selector) {
    return driver.findElement(selector).getAttribute("value");
  },
  setValue: function(selector, value) {
    return driver.findElement(selector).sendKeys(value);
  },
  click: function(selector) {
    return driver.findElement(selector).click();
  },
});
javascript

Conclusion

You have to understand several fundamental concepts regarding Selenium to write effective and stable end-to-end tests. Once you get a grip on e2e testing, you may start to enjoy writing them and your app coverage will improve noticeably. e2e tests are crucial in Single Page Applications (SPAs) when you use a front-end framework such as React.js. Selenium proves a valuable alley on this front; try it out and see how it goes.

20