Writing Better Protractor Tests with Panel Objects

Leave a comment

Larry Van Sickleby 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.

Author: bridge360blog

Software Changes Everything.... Bridge360 improves and develops custom application software. We specialize in solving complex problems at every phase of the software development lifecycle, removing roadblocks to help our clients’ software and applications reach their full potential in any market. The Bridge360 customer base includes software companies and world technology leaders, leading system integrators, federal and state government agencies, and small to enterprise businesses across the globe. Clients spanning industries from legal to healthcare, automotive to energy, and high tech to high fashion count on us to clear a path for success. Bridge360 was founded in 2001 (as Austin Test) and is headquartered in Austin, Texas with offices in Beijing, China.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s