HomeSoftware TestingSmart Home
  
  
Writing maintainable tests with Nightwatch.js page objects and commands

Writing maintainable tests with Nightwatch.js page objects and commands

April 25, 2020

Nightwatch.js page object model and commands API

The page object model in test automation allows you as a test engineer to write more readable and maintainable tests by placing the CSS or XPath selectors for the DOM elements into a single organized object.

The page object centralizes common selectors and logic for portions of the UI relevant to your tests. This page object can expose functions that hold repetitive logic so you don't need to repeat the same code all over your test classes.

Tests become more readable because

  • There is less code overall (less repetitive code)
  • Selectors can change from something like .click('div select option:checked') to .click('@friendlyName')
  • If the UI changes you only need to make the change in one place to fix all your tests

Well-written test automation frameworks like Nightwatch.js support this using their Page Object API which we will cover in this tutorial.

Refactoring tests to use the page object model

The example software under test, SUT, in this course is a simple calculator. It supports addition, subtraction, multiplication, and division of two integers selectable through drop down select elements. The integer range is 0 to 5. Let's also assume for example sake say we need to test this at the UI layer.

Calculator

A basic automated test for this might be to test adding 2+2=4 and look like the example below.

'two plus two is four': function (browser) {
browser
.url('http://localhost:3000')
.setValue('#numList1', 2)
.setValue('#operatorList', '+')
.setValue('#numList2', 2)
.click('#submit')
.expect.element('#result').text.to.equal('4');
}

One can imagine if we were to exhaustively test the calculator we could have 144 tests (6 integers * 6 integers * 4 math operations) plus any other business or UI requirements outside basic math.

This would result in a large amount of tests with the same hardcoded CSS selectors and value setting logic.

If the page interface changed in the future we'd have a test maintenance nightmare 😰

Let's refactor to use page objects

Leveraging the Nightwatch Page Object API we can refactor the above test case to this example below.

'two plus two is four': function (browser) {
let calcPage = browser.page.calculator();
calcPage
.setValue('@firstNumber', 2)
.setValue('@operator', '+')
.setValue('@secondNumber', 2)
.click('@submit')
.expect.element('@result').text.to.equal('4');
}

The CSS DOM selectors are moved to the page object called calculator.js. The CSS selectors in the test are replaced using @elementNameHere placeholders/pointers to the name of the property in the element collection and Nightwatch resolves it to the CSS selector defined in the page object module when the test runs.

module.exports = {
url: 'http://localhost:3000',
elements: {
firstNumber: '#numList1',
secondNumber: '#numList2',
operator: '#operatorList',
result: '#result',
submit: '#submit'
}
}

So now if the selectors on the page ever change you can just make the change in one place, the page object, calculator.js.

This is better, but we still have a lot of repetitive logic here for setting the value...

Let's refactor to use page object commands

We can make the tests use less repetitive code by refactoring it into a command in the page object.

As a bonus, by naming it something descriptive like add(...) it makes the intention of the test more understandable instead of a series of clicks and setValue.

'two plus two is four': function (browser) {
browser.page.calculator()
.add(2, 2)
.expect.element('@result').text.to.equal('4');
}

Considerably more readable and descriptive! 😌

The logic for .add(...) gets added to the calculator page object and the object containing the custom commands gets wired into the page object by adding it to the commands: property as shown toward the bottom.

const calculatorCommands = {
add: function (n1, n2) {
return this.fillAndSubmit(n1, n2, '+');
},
fillAndSubmit: function (n1, n2, op) {
return this
.setValue('@operator', op)
.setValue('@firstNumber', n1)
.setValue('@secondNumber', n2)
.click('@submit');
}
...
module.exports = {
url: 'http://localhost:3000',
commands: [calculatorCommands],
elements: {
...

Moving the common logic to one place instead of scattered in all your test cases has the same advantage of selectors if the functionality of the page changes in the future. For example, maybe there is navigation to the page changes or we change from a drop down to a text box. Fixing the tests only has to be done in one place.

Further refactoring?

So now we have something readable and maintainable like these series of tests.

'0-5=-5': function (browser) {
browser.page.calculator()
.subtract(0, 5)
.expect.element('@result')
.text.to.equal('-5');
},
'2-2=0': function (browser) {
browser.page.calculator()
.subtract(2, 2)
.expect.element('@result')
.text.to.equal('0');
},
'5*2=10': function (browser) {
browser.page.calculator()
.multiply(5, 2)
.expect.element('@result')
.text.to.equal('10');
},
'5 divided by 0 is Infinity': function (browser) {
browser.page.calculator()
.divide(5, 0)
.expect.element('@result')
.text.to.equal('Infinity');
}

You could further optimize as a matter of preference to something like the below example too.

'0-5=-5': function (browser) {
browser.page.calculator()
.subtracting(5).from(0).equals(5)
},
'5 divided by 0 is Infinity': function (browser) {
browser.page.calculator()
.dividing(5).by(0).equals('Infinity')
}

Configuring Nightwatch.js to use page objects

To allow Nightwatch to find your page objects you need to add the page_objects_path to your nightwatch.json file. My example puts it in ./page-objects/ and my tests themselves are in ./tests/

nightwatch.json

{
"src_folders": ["test"],
"page_objects_path": "page-objects",
"selenium": {
"start_process": false
},
"webdriver": {
"start_process": true,
"server_path": "node_modules/chromedriver/lib/chromedriver/chromedriver",
"port": 9515
},
"test_settings": {
"default": {
"desiredCapabilities": {
"browserName": "chrome"
}
}
}
}

Nightwatch expects the page object at that path to be a .js file. The filename you give it will be how it is referenced in your test through the Nightwatch browser object passed through your tests.

For example, you can access the \page-objects\calculator.js page object in your test with syntax

'0-5=-5': function (browser) {
browser.page.calculator().etc.etc;
}

I've posted the entire example for testing my calculator application on my GitHub 👉 Nightwatch.js page objects and drop down select elements tutorial

If you are a Nightwatch beginner and haven't seen my introduction on setting up your Nightwatch project and running tests you'll enjoy the tutorial below

If you have any questions or comments please reach out through my social links below 👇

Photo of David Mello

David Mello

Breaker of software, geek, meme enthusiast.