Using Page Objects with Protractor and Cucumber in Angular Applications

Semaphore

Introduction

In a previous tutorial,
we explained how to couple Cucumber with Protractor tests. This combination
serves well for testing user interactions with your AngularJS applications, as
well as creating living documentation right into your tests. As your testing
needs grow, it's crucial to develop a testing infrastructure that is scalable
and easy to maintain as end-to-end tests take time to develop. In this tutorial,
you'll learn how to incorporate page objects into your test framework — a
model that makes organizing UI elements more efficient.

What is a Page Object Model?

A Page Object Model (POM) is a type of design pattern. This pattern is very popular within the
Selenium ecosystem, so you may have come across it before. The core purposes of
page objects are reducing code duplication and enhancing overall maintainability
of a test suite.

A page object is simply a class that encapsulates the information about elements
used in a particular page. You can view it as the skeleton of your user interface.
Along with these elements, methods are created to interact with them — serving
as the actions for your tests. Then the tests can simply call these methods,
moving the functionality that was originally in the test scenario to the page
object.

This pattern produces clean, readable code. So, when an element in the UI of
your application changes, you only need to edit a page object instead every
test that may have used that element.

Page Objects for Protractor and Cucumber Tests

First, let's take a look at a simple example of a test that does not use page objects.

#test.feature
Feature: Angular Task List
As a basic user
I should be able to add and remove tasks from the task list
So that I can keep track of my tasks
Scenario: Protractor and Cucumber Test
Given I go to "https://angularjs.org/"
When I add "Be Awesome" in the task field
And I click the add button
Then I should see my new task in the list

// features/step_definitions/stepDefinitions.jsvarchai=require('chai');varchaiAsPromised=require('chai-as-promised');chai.use(chaiAsPromised);varexpect=chai.expect;module.exports=function(){this.Given(/^I go to "([^"]*)"$/,function(site){browser.get(site);});this.When(/^I add "([^"]*)" in the task field$/,function(task){element(by.model('todoList.todoText')).sendKeys(task);});this.When(/^I click the add button$/,function(){varel=element(by.css('[value="add"]'));el.click();});this.Then(/^I should see my new task in the list$/,function(callback){vartodoList=angularPage.angularHomepage.todoList;expect(todoList.count()).to.eventually.equal(3);expect(todoList.get(2).getText()).to.eventually.equal('Be Awesome').and.notify(callback);});};

This is a very basic example where we navigate to the Angular website and
interact with the sample Todo component. As you can see from the step definition,
all of our interactions with the page are included right there. There isn't a
lot of code needed for this example, but tests can quickly become unwieldy with
this approach, as the complexity of an application grows.

How would we create a page object for this test? First, let's create a pages
folder under the project's existing features folder. In that folder, we can
add a new file named angularPage.js. Now, we can build out the page object.

A page object typically includes two main sections: locators and methods. We can
move the locators used in the test to a new page object file.

As you can see, we now have an organized list of elements needed for the test.
We can name these objects anything we want, as long as they are easily
recognizable and can be used across tests. Next, we will create the methods
needed to interact with these elements.

For this example, we included three basic methods: navigating to the site,
adding a task, and submitting the task. With the page object in place, we can
now update our step definition to utilize it.

// features/step_definitions/stepDefinitions.jsvarangularPage=require('../pages/angularPage.js');varchai=require('chai');varchaiAsPromised=require('chai-as-promised');chai.use(chaiAsPromised);varexpect=chai.expect;module.exports=function(){this.Given(/^I go to "([^"]*)"$/,function(site){angularPage.go(site);});this.When(/^I add "([^"]*)" in the task field$/,function(task){angularPage.addTask(task);});this.When(/^I click the add button$/,function(){angularPage.submitTask();});this.Then(/^I should see my new task in the list$/,function(callback){vartodoList=angularPage.angularHomepage.todoList;expect(todoList.count()).to.eventually.equal(3);expect(todoList.get(2).getText()).to.eventually.equal('Be Awesome').and.notify(callback);});};

Now that it uses a new page object, the test looks much cleaner, but what
exactly did we do?

First, we made sure to require our angularPage page object and named it
appropriately. Then, for each step we called the needed method. For the final
step, we included our assertions in the test, but still utilized the locator
stored in the page object by setting var todoList to the
object angularPage.angularHomepage.todoList.

When implementing the POM, it's important to decide how you want to build the
structure of your page objects. That may depend on what you want to test.

Organizing Page Objects

There are a few approaches you can consider: by component, by page, and/or by
user workflow. It all depends on your team's needs, but these provide a good
starting point for the structure of your page objects.

Organizing locators by page or view may be the simplest approach as you build
out your suite. For example, you would create a page object for every page
accessible within your application. All elements related to that page would go in their
appropriate file.

pages

homePage.js

aboutPage.js

contactPage.js

Another option is organizing locators by component. These can be modules that
are used across your application such as a search feature. Of course, you can
incorporate both by page and component.

pages

searchComponent.js

homePage.js

aboutPage.js

contactPage.js

Finally, it may be beneficial to organize by a particular user workflow like
creating a new user. These page objects would essentially house all methods
needed to accomplish the workflow.

pages

searchComponent.js

homePage.js

aboutPage.js

contactPage.js

createUserFlow.js

The level of complexity for page objects can vary based on the need. The point
of using a POM is reducing code duplication and increasing efficiency and
maintainability of your testing suite. Before creating a large suite of tests,
first consider the approach your team should take in building your automation suite.

Conclusion

We've covered how to change Protractor and Cucumber step definition to utilize page
objects. We also learned how to create these files as well as how to organize
them based on the structure of an application. Adding the Page Object Model to
your tests is an excellent way to make them even more valuable to the team. It
can be easy to simply spin up tests for the sake of needing tests, without
thinking about how to design your framework. However, like with developing an
application, building an efficient system of tests is just as crucial to
releasing a stable product. If you have any comments and questions, feel free to
leave them in the section below.