TechnologySep 11, 2015

Writing Easy-to-Read, Maintainable Protractor tests for Angular Web Apps

Kelleigh Maroney

If you’re reading this blog, you probably already know how important it is to test your code. You may already be writing unit tests for your web app. Front-end tests (also called end-to-end or E2E tests) are just as essential.

Protractor is a great framework for writing E2E tests:

Protractor is an end-to-end test framework for AngularJS applications built on top of WebDriverJS. Protractor runs tests against your application running in a real browser, interacting with it as a user would. (GitHub)

Protractor tests are great, but they can be confusing, messy, and hard to maintain. There are ways, however, to get around these shortcomings and write readable, maintainable tests for AngularJS web applications.

This blog post is written with the assumption that the reader already has a basic understanding of Protractor and how to write tests and run them. If not, my coworker, Maria Knabe, walks you through the basics.

1. Create an App Navigation File

When approaching the task of writing E2E tests for an existing AngularJS application, take a few minutes to figure out what actions are repeated most frequently by a QA tester, and make these actions into functions in a file for use in your E2E test files.

For example, in my project I have a file called ‘navigation.js’, which contains functions for navigating through my app. Since these are common actions that may be needed in many test cases from many pages of the app, this is a good place to group them together. An example of such a file might look something like this:

// navigation.js var nav = { goHome: function() { element('HomeButton')).click(); }, goToSettings: function() { element('userProfile')).click(); element('userProfile')).element(by.cssContainingText('.dropdown-list', 'My Settings')).click(); }, goToDrafts: function() { browser.driver.get('https://localhost:8080/#/drafts'); }, goToMeasure: function() { browser.driver.get('https://localhost:8080/#/measure'); } };

module.exports = nav;

At the top of each test file, just include your ‘navigation.js’ file like this:

var navigation = require('../navigation.js');

This allows you to call any of your navigation functions by referencing your file import as you would a class:


2. Use a Configuration File for Environment Variables

It is also useful to separate all of your environment variables into another file, which in my case is named ‘env_protractor.js.’ This file is for variables such as the name of the site to be tested or the username and password of the user you’re testing with. If you plan to include Protractor tests as part of your build process, you may need the domain name to be dynamic (i.e. ‘localhost’ for your local environment, but ‘qa’ for your QA environment, etc.).

// env_protractor.js var ENV = { DOMAIN: 'localhost', PORT: '8080', CONTEXT_ROOT: '/', USERNAME: 'Bob', PSWD: 'password' };

module.exports = ENV;

Having this file makes your tests more maintainable and more accessible to other developers on your team.

3. Use Page Objects

To make tests as readable as possible, make page objects for each page of your application. A page object holds constants used on the page (such as all the options in a dropdown), the important elements on the page (such as a button element), and useful functions for testing that page (such as typing in an input box and clicking enter). This separates all the grabbing of page elements from the tests themselves. For example, a test not using a page object might look something like this:

// search_test.js it('should have search typeahead suggestions', function() { element('HomeButton')).click(); element('#search-input').clear().sendKeys('glob'); expect(element.all(by.css('.suggestion').count()).toBeGreaterThan(0); });

But if you used a page object, it could like this:

// header_pg_obj.js var HeaderPage = function() { //Page Elements this.searchBoxInput = element('globalSearchInput'));

// Page Functions for retrieving new elements or simulating user interactions this.getResults = function() { return element.all(by.css('.suggestion')); } };

module.exports = new HeaderPage();

// search_test.js var HeaderPage = require('../page_objects/header_pg_obj.js'); var Navigation = require('../navigation.js'); it('should have search typeahead suggestions', function() { Navigation.goHome(); HeaderPage.searchBoxInput.clear().sendKeys('glob'); expect(HeaderPage.getResults().count()).toBeGreaterThan(0); });

This code is far more readable and there’s no confusion about which element is being referenced because you know exactly what page you’re on. It may seem like a lot of overhead to make page objects, but as you write more and more tests they’ll save you from confusion, painful debugging, and make your tests easier to maintain.

Protractor tests are essential for testing AngularJS web applications. They can be big and messy if you don’t write them with maintainability in mind. By creating navigation and environment variable files and by using page objects you can write maintainable, readable, and reusable tests.