by Larry Van Sickle, Senior Software Engineer
Protractor is a useful tool for end-to-end tests for AngularJS applications using nodejs and Jasmine. Any suite of end-to-end tests will have to be maintained and enhanced as the application being tested changes. The tests can require as much maintenance as the application itself. Best practices in structuring the test code can simplify test maintenance and reduce maintenance costs. A key idea for structuring end-to-end tests is the page object.
Page objects isolate knowledge of the details of an application’s HTML, and provide services to testing code independent of any such knowledge. Many, perhaps most, AngularJS applications are single page applications, so it probably is more appropriate to think of panel objects rather than page objects for an Angular application.
What should a Protractor panel or page object look like? Here is an example for a single page application that lets the users assign some set of items to groups. The application has a group panel, a part of the page that has a list of available groups and controls to create new groups and assign items to groups.
A panel object for the group panel would look like this (JavaScript):
/* * Groups panel object. */ var Groups = function() { this._toggleManageGroupsButton = element(By.className('tst-toggle-manage-groups-button')); this._addItemsToGroupsButton = element(By.className('tst-add-items-to-groups-button')); this._saveGroupButton = element(By.buttonText('Save Group')); this._newGroupElement = element(By.binding('newGroup')); this._groupElements = element.all(By.id('tst-group-checkbox')) .filter(function(e) { return e.isDisplayed(); }); this._createGroupButton = element.all(By.className('tst-create-group-button')).get(0); this._removeGroupButtons = element.all(By.className('tst-remove-group-button')); // Click functions this.clickManageGroupsButton = function() { this._toggleManageGroupsButton.click(); }; this.clickAddItemsToGroupsButton = function() { this._addDevicesToGroupsButton.click(); }; this. clickCreateGroupButton = function() { this._createGroupButton.click(); }; this.clickRemoveGroupButton = function(index) { browser.actions().mouseMove(this._groupElements .get(index)).perform(); this._removeGroupButtons.get(index).click(); }; // Is predicates this.isGroupSelected = function(index) { return this._groupElements.get(index).isSelected(); }; // Get count of groups this.getGroupCount = function() { return this._groupElements.count(); }; // Set a value in the text field this.setNewGroupName = function(name) { this._newGroupElement.clear().sendKeys(name); }; // Add a new group on the Groups panel, this.addNewGroup = function(newGroupName) { this.setNewGroupName(newGroupName); this.clickCreateGroupButton(); }; }; module.exports = Groups;
We are defining an “object” that will be used in multiple test files. In JavaScript we define a function Groups that has functions that the tests will call. The first part of the Groups object is properties for accessing the DOM of the application’s webpage. We give these properties names prefixed with an underscore, a convention in JavaScript for properties and functions that should be treated as private to the object containing them. No other part of the test code should use these properties, only the Groups object. These properties are the only parts of the testing suite that encode any direct knowledge of the HTML of the application. This makes for easy maintenance later if a control name changes since it only has to be updated here in one place instead of in multiple tests.
These accessor properties allow us to access the application DOM in a variety of ways. Protractor provides options for accessing DOM elements by class, id, button text, Angular binding, and more. A few of the possibilities are shown here.
Next in the panel object we have functions for operating on and retrieving DOM elements and summary values such as counts of elements. These are functions used by the testing code to drive the application and check for expected values.
There are functions for clicking on the buttons and controls. Most of these simply call the click() function of an element, but the clickRemoveGroupButton() function requires more. This button is visible only when the mouse is over the button itself or its corresponding text field. Protractor will not click on elements that are not visible and active, so to click this button, the test code must put the mouse over the button, then click the button. This is an example of how we encapsulate low level interface details in the panel object.
Another type of function in the panel object is the predicate, a Boolean function used in the expect() statements of a test. These are used to check that expected conditions are true after the operations of the test. In this example the isGroupSelected() function checks whether a checkbox is selected.
It is also quite common for tests to count occurrences of elements. For this application, a test might count the number of groups displayed before and after a user operation. The getGroupCount() function counts occurrences of the group checkboxes, which will be the number of groups displayed. The setNewGroupName() function inserts a text value in an input field, an action needed when a test is creating a new group. Finally, the addNewGroup() function combines two other operations that will be often performed together into a common operation.
The tests themselves are in separate source files. We will discuss best practices for Protractor tests in a separate post, but here is a quick look at how a test file includes and uses a panel object:
// groupsTest.js var Groups = require('../panelobjects/Groups.js'); describe('The New Order Application', function() { var groupPanel = new Groups(); . . . it('should collapse and open the Manage Groups list', function() { . . . }); });
The key idea of the panel object is that all code for dealing with the DOM is encapsulated in the object. The tests drive the application and check values through functions defined in the page objects. This makes maintenance of the test suite simpler. Most changes to the HTML should require changes only to the associated panel object. Use this powerful test structuring technique to build a better Protractor test suite.