Understand what is needed in end-to-end tests for a better separation of responsibilities
When writing GUI (graphical user interface) tests, it's a good practice to separate responsibilities.
Let's see a Protractor test script that does not separate responsibilities very well, and then I'll explain why that is. Afterward, I'll show you how to make it better using Page Objects.
// specs/sample.spec.js
const helper = require('protractor-helper')
describe('Messages sample app', () => {
beforeEach(() => browser.get('https://example.com/messages'))
it('successfully sends a message', () => {
const thankYouHeading = element(by.className('thanks'))
sendMessage(
'John',
'johndoe@example.com',
'Hi, this is a test message'
)
helper.waitForElementVisibility(thankYouHeading)
})
function sendMessage(name, email, message) {
const nameField = element(by.id('name'))
const emailField = element(by.id('email'))
const messageField = element(by.id('message'))
const submitButton = element(by.css('button[type="submit"]'))
helper.fillFieldWithText(nameField, name)
helper.fillFieldWithText(emailField, email)
helper.fillFieldWithText(messageField, message)
helper.click(submitButton)
}
})
Test files (*.spec.js
) should describe how the application behaves in different scenarios. By looking at them, you should understand:
- What the pre-conditions for the tests are (e.g.,
beforeEach(...)
) - What actions every test performs (e.g.,
sendMessage(...)
) - And last but not least, what are the expected results for each scenario (e.g.,
helper.waitForElementVisibility(thankYouHeading)
.)
Anything else from the above-list is a distraction. Test cases are specifications of the application behaviors in different situations. This is why we suffix them with .spec
.
Going back to our existing tests, I can see at least two major distractions. They are:
- The definitions of elements
- And the
sendMessage
function
But what if we could have them on a different module, called page
maybe?
Well, we actually can.
Simplifying, Page Objects are an abstraction used in testing automation, where we define elements that could be present on a web page, and methods (which are actions that we could perform if we were using such a web page.)
A sample Page Object for our previous test example could be defined as follows.
// page-objects/sample.js
const helper = require('protractor-helper')
const nameField = element(by.id('name'))
const emailField = element(by.id('email'))
const messageField = element(by.id('message'))
const submitButton = element(by.css('button[type="submit"]'))
const thankYouHeading = element(by.className('thanks'))
function sendMessage(name, email, message) {
helper.fillFieldWithText(nameField, name)
helper.fillFieldWithText(emailField, email)
helper.fillFieldWithText(messageField, message)
helper.click(submitButton)
}
module.exports = {
nameField,
emailField,
messageField,
submitButton,
thankYouHeading,
sendMessage
}
Our test could then require such a page and use all of its elements and methods, as needed (see below.)
// specs/sample.spec.js
const helper = require('protractor-helper')
const page = require('../page-objects/sample.js')
describe('Messages sample app', () => {
beforeEach(() => browser.get('https://example.com/messages'))
it('successfully sends a message', () => {
page.sendMessage(
'John',
'johndoe@example.com',
'Hi, this is a test message'
)
helper.waitForElementVisibility(page.thankYouHeading)
})
})
Now, the test is mostly concerned with exactly what it should be doing, which is testing! On the other hand, the Page Object deals with locating elements and defining ways of interacting with them (functions).
This way, we're now better at separating responsibilities, and our test suite is easier to maintain.
This post was translated to Portuguese, and you can find it on the Talking About Testing blog.
Top comments (0)