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.js var chai = require ( ' chai ' ); var chaiAsPromised = require ( ' chai-as-promised ' ); chai . use (chaiAsPromised); var expect = 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 () { var el = element ( by . css ( ' [value="add"] ' )); el . click (); }); this . Then ( / ^ I should see my new task in the list $ / , function ( callback ) { var todoList = 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.

' use strict ' ; module . exports = { angularHomepage : { taskList : element ( by . model ( ' todoList.todoText ' )), taskButton : element ( by . css ( ' [value="add"] ' )), todoList : element . all ( by . repeater ( ' todo in todoList.todos ' )) } });

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.

' use strict ' ; module . exports = { angularHomepage : { taskList : element ( by . model ( ' todoList.todoText ' )), taskButton : element ( by . css ( ' [value="add"] ' )), todoList : element . all ( by . repeater ( ' todo in todoList.todos ' )) }, go : function ( site ) { browser . get (site); }, addTask : function ( task ) { var angular = this . angularHomepage ; angular . taskList . sendKeys (task); }, submitTask : function () { var angular = this . angularHomepage ; angular . taskButton . click (); } };

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.js var angularPage = require ( ' ../pages/angularPage.js ' ); var chai = require ( ' chai ' ); var chaiAsPromised = require ( ' chai-as-promised ' ); chai . use (chaiAsPromised); var expect = 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 ) { var todoList = 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.