Nightwatch Style Guide

Adam Winters
Technology at NPR
Published in
5 min readAug 23, 2018

--

Do a Google search for Nightwatch Style Guide, and you might come up with a big ol’ goose egg. (Author’s note: I’ve now been informed that when you do that Google search, I am the first result. Oh, the times they are a-changin’.) At NPR, upon first using Nightwatch, we lived a carefree life, free from style and best practices. In short, we did what we wanted. For smaller testing setups, that can work effectively. But as our testing grew, and more and more people felt comfortable writing their own Nightwatch tests across multiple repositories, one could hear the distant flapping of roost-bound chickens. What our Nightwatch needed was some style!!

nightwatch
|--- commands/ Contains our custom commands.
|--- config/ Contains secrets using dotenv & config packages
|--- local.json
|--- drivers/ Stores Gecko or Chrome drivers
|--- jenkins/ Config files to link with Jenkins server
|--- page_objects/ Page specific commands / elements
|--- subfolder/
|--- myPageObjectFile.js
|--- another_subfolder/
|--- anotherCamelCasedPageObjectFile.js
|--- reports/ nightwatch-html-reporter package folder
|--- screenshots/ Stores screenshots taken during test running
|--- tests/ Holds our tests
|--- My_Very_Great_Test.js
|--- Another_Test_Capital_Snake_Cased.js
|--- videos/ nightwatch-video-recorder package folder
|- globals.js Stores non-environment specific globals
|- nightwatch.conf.js Nightwatch configuration
|- package.json Dependencies and test running scripts
|- README.md Setup from scratch and how to guide.
|- .gitignore Config, reports, screenshots, videos, etc.

We keep our folder names short, descriptive, and snake-cased. Some folders will demand the need for sub-folders, specifically page_objects and tests, as they balloon in files and yearn for better internal organization, and the same rules will reply.

The keen reader will notice that we follow two different rules of file naming for our page objects and tests. We do this for quick identification. Let’s say we want to verify our login page is working. It will probably include a page object for the login page loginPage.js and a similar test Login_Page.js. Thanks to our rules for naming, I can now easily see at a glance that one of those is a page object and the other is a test without needlessly having to add the word Test to every test file.

Okay, so now I’ve organized the structure, but what goes where? Looking across several different Nightwatch projects, I noticed that what gets included within tests and page object files varied pretty drastically. Should a command like this be included in a test or page object: .click('element-selector')? To answer that question, I thought it best to answer this first: Are tests for developers or non-developers? Are they for QA masters or Padawans?

At NPR, we have people outside the QA circle that are invested in the outcome of our tests. Let’s imagine that same login test from earlier failed at this step .click('element-selector') and a non-developer wanted to know why. Maybe that element selector is explicit enough that they understand what’s happening, but maybe it isn’t. If they can’t understand what the test is doing, they aren’t able to manually reproduce the error. Now imagine instead of having that click within our test, we put that inside a function within our loginPage page object and called it clickStaySignedIn. Now, when someone unfamiliar with the test looks through it, they can more clearly understand what they need to do to manually test the same functionality. (Click the “Stay Signed In” checkbox.) Think of tests as a place that a non-developer could comfortably read and understand without breaking a sweat.

Using this criteria, here’s what an example test might look like:

const USERNAME = "testuser";
module.exports = {

before(client) {
// Any additional test setup steps, well explained.
},

'01. Launch login page and size window.': function (client) {
client.page.loginPage().navigate();
client.windowSetPositionAndSize();
},

'02. Login with the test user.': function (client) {
client.page.loginPage()
.verifyPageElements()
.enterUsername(USERNAME)
.enterPassword(client.globals.password)
.clickStaySignedInButton()
.clickLoginButton();
},
'03. Verify log in worked.': function (client) {
client.page.homePage()
.verifyPageElements();
},
'04. End': function (client) {
client.end();
},

};

Can you understand that? I hope so! Even with zero familiarity with this test, someone could easily look through it and understand what it does. Now if anyone needs to reproduce this test manually, they can do it with ease. A few other things to note about the test organization: the top of the test should include all constants used within the test (when possible). This allows for much easier updating. But remember(!), constants should never be functions or a series of testing steps/assertions. Those things should be moved into page objects or custom commands. Use descriptive, numbered steps to break up the tests. Those, in addition to the nightwatch-html-reporter, help create a nice, easy-to-read report of what passed/failed. And it’s better to have shorter steps than longer ones. If the description gets to be too long, maybe it’s time to break that step into multiple ones.

As for the page object, that might look something like this:

const loginPageCommands = {

verifyPageElements() {
return this
.waitForElementVisible('@loginForm')
.assert.visible('@userField');
},
/**
* Types in given username into login field.
* @param {string} username - Username to fill in.
*/
enterUsername(username) {
this.api.useXpath()
.waitForElementVisible('@userField')
.click('@userField');
.setValue('@userField', username)
this.api.pause(500);

return this;
},
etc...};

module.exports = {
commands: [loginPageCommands],
elements: {
loginForm: {
selector: "//form[@id='login']",
locateStrategy: 'xpath',
},
userField: {
selector: '#userField',
},
anotherElement: '[name="end_mer1"]',
},
};

I provide this as an example, but there’s really no wrong way to organize and code your page objects. Just keep in mind that if it seems really complicated, document it. If it’s doing too much, break it up. If a similar function is used over and over in different page objects, think about making it a custom command.

Some other odds and ends…

Test Running: Inside your package.json, include a script to run every test you have individually. Keep the name of the test command between 1–3 words and self-evident to what will happen when it runs. As your amount of tests grow, this might seem like a lot of scripts, but there will always be a need to run tests one at a time for troubleshooting purposes.

"login-test": "nightwatch --env stage1 --test tests/Login_Page.js"

console.log: Not inside the test, you don’t! Naturally, there are times where it is necessary in error capturing and information handling inside functions. (But look above and you’ll remember: functions are kept within page objects, not tests.) Also, remember Nightwatch offers optional log messaging for most commands/assertions.

Custom Commands: If functionality is shared across page objects or tests, add it to the commands folder. However, each file should be only one custom command. Create sub-folders if you think it’s ballooning into outrageousness.

Globals: While some globals are best set inside globals.js, others can be set within page objects or commands in lieu of promises or asynchronous functions.

Pauses: You can either add them directly into the test as “client.pause(TIME)” or inside the page object on any Nightwatch command. Up to you!

Password Management: Since there is always need to have values outside of GitHub, password management is a must. To do this, we use the node packages dotenv and config. dotenv hides and sets variables locally within an .env file and config manages those same secrets in a config file within our codebase (ignored by GitHub, naturally!). Here’s what it might look like in your codebase. The first is a before hook in the globals.js and the second would be what it looks like in a page object.

before: function (done) {
require('dotenv').config({path: process.env.HOME + '/Directory/.env'});
console.log("Looking for secrets in: " + process.env.HOME + '/Directory/.env' + " or inside `local.json`");
done();
}
--------------------------------------------------------------------const pw = process.env.PASSWORD ? process.env.PASSWORD : config.get('password');

I hope you found some of these rules helpful, and happy styling!

--

--