Automated Regression Testing

Cucumber and Selenium with Node.js

Posted by Ben Frey on August 21, 2018

Over the lifespan of a large product, how are you ensuring that your existing code still functions as you expect? How many of those scenarios are you having to cover with manual testing? My team has been utilizing Cucumber for many years to handle our regression testing for back-end services. More recently, we started using Selenium to do UI regression testing. Using these tools has enabled us to do just minimal manual testing each sprint while increasing the chances that we will find negative impacts to other parts of the application.

Cucumber and Selenium both have Node libraries available with decent documentation. In this post, I want to walk through a simple example to show how you might test a UI interaction using both libraries.

Project Setup

We’ll set up our Node.js project with a structure like this:

/features
    google.feature
    /step_definitions
        googleSteps.js
    /support
        worldConstructor.js
package.json

The Cucumber documentation recommends your code live within /features and any sub directories. Before we can write any tests, we need a browser to run them in, and something to drive the process. Cucumber provides hooks for running logic at certain events during your tests. We want to initialize our chromedriver before any tests are run, so we will use the BeforeAll hook. Afterwards we need to shut down the driver.

require('chromedriver');
const {BeforeAll, AfterAll} = require('cucumber');
const seleniumWebdriver = require('selenium-webdriver');

let driver;

BeforeAll(function () {
    driver = new seleniumWebdriver.Builder()
        .forBrowser('chrome')
        .build();
});

AfterAll(function () {
    return driver.quit();
});

We’ve started up our driver, but we need a way for the steps we are going to write to use the driver. In Cucumber.js, a World is an object available as this to all of our step functions. Let’s make the driver available in the World and use the synchronous call setWorldConstructor to put our World in scope:

require('chromedriver');
const {BeforeAll, AfterAll, setWorldConstructor} = require('cucumber');
const seleniumWebdriver = require('selenium-webdriver');

function World(driver) {
    this.driver = driver;
}

let driver;

BeforeAll(function () {
    driver = new seleniumWebdriver.Builder()
        .forBrowser('chrome')
        .build();
    setWorldConstructor(World.bind(undefined, driver));
});

AfterAll(function () {
    return driver.quit();
});

Let’s try running it and we should see the driver quickly open and close. Cucumber can be run directly with ./node_modules/.bin/cucumber-js or via node with node node_modules/cucumber/bin/cucumber.js

This is the expected console output at this point:

search scenario

Writing a feature file

Let’s test something that we’ve probably all done at one time or another: a Google search. First we’ll specify our feature name and tag it:

@all
Feature: Google Search

Tags allow you to specify a scope for features and scenarios. A common pattern my team has used is to switch between API and UI specific tests. Tags are optional and can be added at the feature level and for each scenario individually. You can specify which tags you’d like to run (or not run) by appending an option like this when you run Cucumber: --tags "@all and not @ignore"

For the search, we want to enter some text then click the search button.

search scenario

Due to the dynamic nature of search results, we’ll just verify that the browser navigates to a different URL and that the search result count indicates multiple results, rather than look for a specific hit.

search scenario

Features are written in a BDD Given -> When -> Then format. Given steps are used to set up a test scenario by setting up data or getting the website to a specific state. When steps are actions that a user would take in the test scenario. Then steps are the verifications/assertions of the state after the action is taken. Any Given, When, or Then after the first becomes an And step to make it more readable.

For this feature file, any tests we write will likely have navigation to google.com as the first step, regardless of what other components we are testing. Cucumber provides a Background header for this purpose. Any steps under Background will be run for every Scenario in the file.

@all
Feature: Google Search

  Background: load google homepage
    Given the user has navigated to the Google search homepage

  Scenario: conduct search
    When the user types cucumber in the search bar
    And the user clicks Google Search
    Then the browser navigates to the search result page
    And the search result has more than 1 hit

Running Cucumber again indicates that it picked up our scenario, but it couldn’t find any step definitions. It also provides an example snippet that can be used directly to write our step definitions.

undefined steps

Writing step definitions

The snippet that the Cucumber runner provided is a good start, but since Selenium WebDriver deals in promises we can omit the callback and return our promises to Cucumber. Also, we will want the ability to capture variables from the step text, enabled by using regular expressions in place of strings in our step definitions. We don’t need a variable for this first step, but rather than switch between string and regex matchers for different steps, let’s stick to regex. For our first step (navigating to Google search page) we can use the Selenium WebDriver get method.

const {Given} = require('cucumber');

Given(/^the user has navigated to the Google search homepage$/, function () {
    return this.driver.get('https://www.google.com');
});

When Cucumber matches your feature steps to step definitions, it will ignore the first word. Given, When, Then are just proxies of the same internal method in Cucumber.

Moving through our feature file, the next unimplemented step is for typing in the search bar. To do that, we need to locate the search bar in the DOM. We could use the Selenium WebDriver’s findElement method, but Cucumber would move from the page load step to the search bar step without waiting for page to completely resolve. If the search bar wasn’t loaded in time, our second step would fail.

A good practice in your Selenium steps is to always make sure an element is available before trying to interface with it. I recommend doing this even for steps that are after other verification steps (Thens), as you may later re-order your steps or use them in different combinations in other tests. Instead of findElement, we can use wait(until.elementLocated) which will wait until the specifed element is located or a set amount of time has passed. If the element is located before the timeout occurs, wait will return that element in a WebElementPromise. With the element located, we can then invoke sendKeys.

Since we may want to switch up what gets typed into the search bar for different tests, let’s use a regex capture group. Our step function then takes the captured variables as parameters in the order they appear in the regex, followed by a data table if you have one, then a callback if you use the callback interface.

const {When} = require('cucumber');

/* step function takes the captured variables as parameters in the order they appear in the  */
/* regex, then a data table if you have one, then a callback if using the callback interface. */
When(/^the user types (.+) in the search bar$/, function (searchText) {            
    /* wait a max time of 5 seconds. */
    return driver.wait(until.elementLocated({xpath: `//input[@title='Search']`}), 5000) 
    /* sendKeys takes an argument list of strings and/or Keys which will get sent in the */
    /* sequence provided. Strings will get broken down into individual characters. */
        .then(element => element.sendKeys(searchText));                                 
});

The element locator can be a webdriver By or a shorthand object that By understands. I prefer XPath locators due to their flexibilty and readability, but By supports CSS selectors as well. The Elements tab in Chrome Developer Tools allows you to search the DOM by XPath and is a good place to test your locator strings.

Our next step is to click on the search button. Like the previous step, let’s make the step more generic by making the button text be a variable. Let’s refactor the wait call into a separate function since we will want to re-use it here to find the button. We can then call click on the located button.

/* `this.driver` is only available within our Cucumber functions, so we must pass the driver */
/* as a parameter to helper functions. */
const locateElement = (driver, elementLocator) => {  
    return driver.wait(until.elementLocated(elementLocator), 5000, 'Element not found within time limit');
};

When(/^the user clicks (.+)$/, function (buttonValue) {
    return locateElement(this.driver, {xpath: `//input[@type='button'][@value='${buttonValue}']`}).click();
});

Our first verification is that the browser ends up on the search result page. We can use getCurrentUrl and an assertion library like chai to do the check.

const {expect} = require('chai');
const {Then} = require('cucumber');

Then(/^the browser navigates to the search result page$/, function () {
    return this.driver.getCurrentUrl()
        .then(url => expect(url).to.have.string(`https://www.google.com/search`));
});

Finally, we want to check that the results page indicates at least a certain number of hits. getText will return all text contained within that element and its children.

Then(/^the search result has more than (\d+) hit$/, function (minCount) {
    return locateElement(this.driver, {xpath: `//div[@id='resultStats']`}).getText()
        .then((text) => {
            const resultCount = parseInt(text.replace(/About ([\d,]+) results.+/, '$1').replace(/,/, ''), 10);
            expect(resultCount).to.be.above(minCount);
        });
});

Running the Cucumber now should yield a passing scenario.

completed steps

posted on August 21, 2018 by
Ben Frey