/
Automated Browser Testing With Nightwatch & Cucumber

Automated Browser Testing With Nightwatch & Cucumber

What Is Nightwatch?

Nightwatch is a thin JavaScript API overlaying the Selenium WebDriver API. This allows us to control the Selenium browser automation from JavaScript code in a manner most familiar to JavaScript and UI developers.

What is Cucumber?

Cucumber is library for implementing Behavior Driven Development. IT codifies a simple format (Gherkin) for writing specifications for a system which should be readable by both technical contributors and non-technical users/stakeholders alike.

Why Would I Use Them Together?

When you combine Nightwatch and Cucumber, writing test scenarios becomes relatively painless, and attaching logic to those scenarios is very easy. The real value comes from the fact that when paired, you get exceptional reporting on those test scenarios. You can even include screenshots of the automated browser in your reports!

Setting It All Up

Install Dependencies

  1. Install Nightwatch

    yarn add --dev nightwatch nightwatch-api
  2. Install CucumberJS

    yarn add --dev cucumber-js
  3. Install Cucumber Reporting Libraries

    yarn add --dev cucumber-pretty cucumber-html-reporter

Create Configuration

  1. Create the nightwatch configuration

    1. From the command-line, run nightwatch to cause it to generate an initial configuration file

      $ nightwatch No config file found in the current working directory, creating nightwatch.conf.js in the current folder... Error: An error occurred while trying to start the Nightwatch Runner: The path to the GeckoDriver binary is not set. You can either install geckodriver from NPM: npm install geckodriver --save-dev or download GeckoDriver from https://github.com/mozilla/geckodriver/releases, extract the archive and set "webdriver.server_path" config option to point to the binary file.
    2. Modify the nightwatch.conf.js file as follows:

      // Autogenerated by Nightwatch // Refer to the online docs for more details: https://nightwatchjs.org/gettingstarted/configuration/ const Services = {}; loadServices(); module.exports = { // An array of folders (excluding subfolders) where your tests are located; // if this is not specified, the test source must be passed as the second argument to the test runner. src_folders: ['tests/e2e'], // See https://nightwatchjs.org/guide/working-with-page-objects/ page_objects_path: '', // See https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-commands custom_commands_path: '', // See https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-assertions custom_assertions_path: '', // See https://nightwatchjs.org/guide/#external-globals globals_path: '', webdriver: { start_process: true, server_path: 'node_modules/.bin/chromedriver', port: 9515, }, test_settings: { default: { disable_error_log: false, launch_url: 'http://localhost:3000/', screenshots: { enabled: false, path: 'screenshots', on_failure: true, }, desiredCapabilities: { browserName: 'chrome', }, webdriver: {}, }, chrome: { screenshots: { enabled: false, path: 'screenshots', on_failure: true, }, desiredCapabilities: { browserName: 'chrome', chromeOptions: { // This tells Chromedriver to run using the legacy JSONWire protocol (not required in Chrome 78) // w3c: false, // More info on Chromedriver: https://sites.google.com/a/chromium.org/chromedriver/ args: [ // '--no-sandbox', // '--ignore-certificate-errors', // '--allow-insecure-localhost', // '--headless' ], }, }, }, /// /////////////////////////////////////////////////////////////////////////////// // Configuration for when using the browserstack.com cloud service | // | // Please set the username and access key by setting the environment variables: | // - BROWSERSTACK_USER | // - BROWSERSTACK_KEY | // .env files are supported | /// /////////////////////////////////////////////////////////////////////////////// browserstack: { selenium: { host: 'hub-cloud.browserstack.com', port: 443, }, // More info on configuring capabilities can be found on: // https://www.browserstack.com/automate/capabilities?tag=selenium-4 desiredCapabilities: { 'bstack:options': { local: 'false', userName: '${BROWSERSTACK_USER}', accessKey: '${BROWSERSTACK_KEY}', }, }, disable_error_log: true, webdriver: { keep_alive: true, start_process: false, }, }, 'browserstack.chrome': { extends: 'browserstack', desiredCapabilities: { browserName: 'chrome', chromeOptions: { // This tells Chromedriver to run using the legacy JSONWire protocol // More info on Chromedriver: https://sites.google.com/a/chromium.org/chromedriver/ w3c: false, }, }, }, 'browserstack.firefox': { extends: 'browserstack', desiredCapabilities: { browserName: 'firefox', }, }, 'browserstack.ie': { extends: 'browserstack', desiredCapabilities: { browserName: 'IE', browserVersion: '11.0', 'bstack:options': { os: 'Windows', osVersion: '10', local: 'false', seleniumVersion: '3.5.2', resolution: '1366x768', }, }, }, /// /////////////////////////////////////////////////////////////////////////////// // Configuration for when using the Selenium service, either locally or remote, | // like Selenium Grid | /// /////////////////////////////////////////////////////////////////////////////// selenium: { // Selenium Server is running locally and is managed by Nightwatch selenium: { start_process: true, port: 4444, server_path: Services.seleniumServer ? Services.seleniumServer.path : '', cli_args: { 'webdriver.gecko.driver': Services.geckodriver ? Services.geckodriver.path : '', 'webdriver.chrome.driver': Services.chromedriver ? Services.chromedriver.path : '', }, }, }, 'selenium.chrome': { extends: 'selenium', desiredCapabilities: { browserName: 'chrome', chromeOptions: { w3c: false, }, }, }, 'selenium.firefox': { extends: 'selenium', desiredCapabilities: { browserName: 'firefox', 'moz:firefoxOptions': { args: [ // '-headless', // '-verbose' ], }, }, }, }, }; function loadServices() { try { Services.seleniumServer = require('selenium-server'); } catch (err) {} try { Services.chromedriver = require('chromedriver'); } catch (err) {} try { Services.geckodriver = require('geckodriver'); } catch (err) {} }
  2. Create the cucumber configuration file <root>/cucumber.conf.js

    const fs = require('fs'); const path = require('path'); const { setDefaultTimeout, After, AfterAll, BeforeAll } = require('cucumber'); const { createSession, closeSession, startWebDriver, stopWebDriver, getNewScreenshots, } = require('nightwatch-api'); const reporter = require('cucumber-html-reporter'); setDefaultTimeout(60000); BeforeAll(async () => { await startWebDriver({}); await createSession({}); }); AfterAll(async () => { await closeSession(); await stopWebDriver(); setTimeout(() => { reporter.generate({ theme: 'bootstrap', jsonFile: 'report/cucumber_report.json', output: 'report/cucumber_report.html', reportSuiteAsScenarios: true, launchReport: true, metadata: { 'App Version': '0.3.2', 'Test Environment': 'POC', } }); }, 0); }); After(function() { getNewScreenshots().forEach(file => this.attach(fs.readFileSync(file), 'image/png'), ); });
  3. Add script entries to package.json for running the Cucumber tests

    "test:cucumber": "cucumber-js --require cucumber.conf.js --require features --require features/step-definitions --format node_modules/cucumber-pretty --format json:report/cucumber_report.json",
  4. Create the directory <root>/features/step-definitions

Create Your First Specification

  1. Create your first .feature file as <root>/features/Login.feature with the following contents

    Feature: Login & Authentication Scenario: Failed login Given I open a browser to the login page When I enter an e-mail address of "tester@pathckeck.org" And I enter a password And I click the Login button Then I expect to see a Toast alert containing "Something went wrong"
  2. Define steps to satisfy your feature steps in the file <root>/features/step-definitions/steps.js

    const { client } = require('nightwatch-api'); const { Given, Then, When } = require('cucumber'); Given(/^I open a browser to the login page$/, async () => { return await client .url('http://localhost:3000/login') .waitForElementVisible('body', 5000); }); When(/^I enter an e-mail address of "([^"]*)"$/, email => { return client.setValue('input[id=email-input]', email); }); When(/^I enter a password$/, () => { return client.setValue('input[id=pass-input]', 'supersecretpasword'); }); When(/^I click the Login button/, () => { return client.click('button[id=login-button]'); }); // This step definition extracts a parameter from the scenario // using a regular expression Then(/^I expect to see a Toast alert containing "([^"]*)"$/, message => { return client .waitForElementVisible('div[role=alert]') .expect.element('div[role=alert]') .text.to.contain(message); });

Run Your First Scenario

  1. Run the tests and produce a report using npm run test:cucumber.

    1. Once the tests complete, your browser should open to show an HTML report like this:

Implement Your Own Scenario

  1. Edit the <root>/features/Login.feature file and create a new scenario called ‘Prevent login submission when invalid e-mail is present’

    1. Example:

      Scenario: Prevent login submission when invalid e-mail is present Given I open a browser to the login page When I enter an e-mail address of "invalid.org" And I enter a password of "supersecretpassword" Then I expect that the Login button is disabled
    2. IF your IDE supports Cucumber/Gherkin, you might see the steps which do not have definitions yet highlighted. like this:

       

    3. In IntelliJ/WebStorm, you should be given the option to implement those steps.

    4. Otherwise, add new steps to <root>/features/step-definitions/steps.js for the 2 new steps

  2. Implement steps in <root>/features/step-definitions/steps.js

    // Extracts the password to be used via a regular expression pattern When(/^I enter a password of "([^"]*)"$/, password => { return client.setValue('input[id=pass-input]', password); }); // Assert that the "disabled" attribute of the Login button is set to "true" Then(/^I expect that the Login button is disabled$/, () => { return client.assert.attributeEquals( 'button[id=login-button]', 'disabled', 'true', ); });
  3. Run both scenarios with npm run test:cucumber and you should see a report like the following: