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
Install Nightwatch
yarn add --dev nightwatch nightwatch-api
Install CucumberJS
yarn add --dev cucumber-js
Install Cucumber Reporting Libraries
yarn add --dev cucumber-pretty cucumber-html-reporter
Create Configuration
Create the nightwatch configuration
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.
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) {} }
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'), ); });
Add
script
entries topackage.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",
Create the directory
<root>/features/step-definitions
Create Your First Specification
Create your first
.feature
file as<root>/features/Login.feature
with the following contentsFeature: 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"
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
Run the tests and produce a report using
npm run test:cucumber
.Once the tests complete, your browser should open to show an HTML report like this:
Implement Your Own Scenario
Edit the
<root>/features/Login.feature
file and create a new scenario called ‘Prevent login submission when invalid e-mail is present’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
IF your IDE supports Cucumber/Gherkin, you might see the steps which do not have definitions yet highlighted. like this:
In IntelliJ/WebStorm, you should be given the option to implement those steps.
Otherwise, add new steps to
<root>/features/step-definitions/steps.js
for the 2 new steps
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', ); });
Run both scenarios with
npm run test:cucumber
and you should see a report like the following: