Pro tips for E2E protractor tests

Pro tips for E2E protractor tests

What is an E2E test?

Unit testing are from the code perspective. End to end tests are from the user perspective. It means that the tests are supposed to actually emulate a real user, it means actually opening a browser, clicking on stuff and see that the stuff is working or not working according to the spec. It is extremely important, because unlike manual regression tests, those tests can be done over and over again and does not miss anything and the QA team can focus their attention to create new regression tests.

What is a protractor?

Protractor is an end-to-end test framework for AngularJS applications. Protractor runs tests against your application running in a real browser, interacting with it as a user would.

Creating tests can be hard!

Protractor is very easy to use and create, but it can be hard to maintain and avoid false-positives. In order to smooth the creation and maintance process, there are several rules that you should follow.

#1 tests are node.js applications!

protractor tests are basically written in node.js and we should handle those as node.js application. It means that all the good practices that should be implemented in node.js should be in the protractor tests as well. For example

  1. Avoid callback hell by using events or promises.
  2. Use static code analysis tool (like eslint) to enforce code conventions.
  3. Use strict mode to avoid common errors.
  4. Use pull request mechanism on the tests repository.

and a lot more. Treat your test repository as a whole node.js project. Including extensive readme file with a lot of information on setting up and start to work with.

#2 Include all hard code data inside the config file

Every test have hard code data. For example user name & password, name of a menu item and other strings that the user see. Include all shared hard code data inside the config file. Exactly like that:

params: {
  login: {
    user: 'myUser',
    password: ‘xxxxx’,
    userAdmin: 'myAdmin',
    passwordAdmin: ‘xxxxx’
  },
  timeout: {
    ECstandards: 3000
  }
},

You can call those parameters from any place in the test code by using browsers.params. for example:

browser.params.timeout.ECstandards

#3 Use page objects

The most important part in writing good, solid and robust tests is to use page objects. Page objects are just a fancy name to actual node module that you create and contain bank of elements. You can and you should create different module for each part of the application. for example, the login page or modal, the user management page or state, etc. It is also very convenient to allocate page objects to processes. for example to a login\logout process.

Here is a live, working example to page object:

'use strict';

var myPage = function () {    
    browser.driver.manage().window().maximize();
};

myPage.prototype = Object.create({}, {  

  loadingBarSettings: { get: function () { return element(by.xpath('//div[@class="snake-foreground"]/div[3]')); } },  

  productNameHeader: { get: function () { return element(by.css('h2.ng-binding')); } },  

  productContentOwnersButton: { get: function () { return element(by.xpath('//span[contains(@class, "my-badge-text") and text() = "Content Owners"]')); } },  

  tooltip: { get: function () { return element(by.xpath('//div[@class="tooltip-inner"]')); } },  settingsButton: {get: function () { return element(by.xpath('//a[contains(@class, "product-settings")]')); } },  

  contentOwnersLabel: { get: function () { return element(by.xpath('//ul[contains(@class, "chosen-choices")]')); } },  saveSettingsButton: { get: function () { return element(by.xpath('//span[contains(@class, "ng-scope") and text() = "Save"]')); } },  

  inputContentOwner: {get: function () { return element(by.xpath('//li[contains(@class, "search-field")]')); } },  contentOwnerMalina: {get: function () { return element(by.xpath('//ul[@class="chosen-results"]//li[.="malina"]')); } },  

  contentOwnerMosest: {get: function () { return element(by.xpath('//ul[@class="chosen-results"]//li[.="mosest"]')); } },  deleteContentOwnerMalina: {get: function () { return element(by.xpath('//ul[@class="chosen-choices"]/li[2]/a')); } },  

  deleteContentOwnerMosest: {get: function () { return element(by.xpath('//ul[@class="chosen-choices"]/li[2]/a')); } },  backToProductButton: { get: function () { return element(by.xpath('//span[contains(@class, "text ng-binding") and text() = "Back to Operations Orchestration"]')); } },  

  popupContentOwnersTitle: { get: function () { return element(by.xpath('//h4[contains(@class, "modal-title") and text() = "Content Owners"]')); } },  contentOwnersList: { get: function () { return element(by.xpath('contentowners')); } },  

  popupContentOwnersCloseButton: { get: function () { return element(by.xpath('//button[contains(@class, "btn btn-primary") and text() = "Close"]')); } },  contentOfferingTile: { get: function () { return element(by.xpath('//h4[contains(@class, "ng-binding") and text() = "my Screen Recorder Add-In 4"]')); } },  

  contentOfferingTitle: { get: function () { return element(by.xpath('//h3[contains(@class, "page-title ng-binding")]')); } },  contentOfferingSettingsButton: { get: function () { return element(by.xpath('//a[contains(@class, "contentoffering-settings pull-right ng-scope")]')); } },  

  coContentOwners: { get: function () { return element(by.xpath('//h5[contains(@class, "m-b-2") and text() = "Content Owners"]/..')); } },  coContentOwnersLabel: { get: function () { return element(by.xpath('//label[text() = \'Content Owners\' and @for=\'contentOfferingsContentOwners\']')); } },  

  backToContentOfferingButton: { get: function () { return element(by.xpath('//span[contains(@class, "text ng-binding") and text() = "Back to my Screen Recorder Add-In 4"]')); } }

});

module.exports = myPage;

You can see it is basically contains a lot of elements. In the test file I will require the module like every node.js module and then I can use it in that way:

myPage = new myPage();

browser.wait(EC.presenceOf(myPage.productNameHeader), browser.params.timeout.ECstandards, 'product header is not there');

myPage.productContentOwnersButton.click().then(function () {
  //Do something after click
)};

#4 Use short XPath selectors

Selectors are the main ingredient of the tests, we are using selector to select elements for clicking, asserting or anything else. Protractor allow us a lot of selectors (called locators in protractor) described here: https://github.com/angular/protractor/blob/master/docs/locators.md

However, the main selectors that we will used will be based on xpath and it cannot be avoided in Protractor. 

Choosing a selector is an art, there is a difference between

“/html/body/div/div/div[2]/div/div[3]/div/div[2]/div[2]/div/form/fieldset/ng-transclude/div[1]/div[1]/div[2]/div[1]/label”

And

//label[text() = ‘Content Files’]”

This is the same element, but You should choose meaningful XPath selector that is easy to debug.

#5 Use text in the XPath

The best xpath must have the exact text that the user see. CSS classes and ID are good selectors but are not what the user see. The user see buttons and texts, so xpath using those texts are good emulation of user decision. Make sure that your xpath have text inside them. Best if to use text only. There are a lot of Xpath selectors based on text.

For example, This selector:

element(by.xpath('//a[text() = "Logout"]'));

Will work for ANY Logout link that is present on the page!

#6 User error handling in Wait until condition

Sometimes we need to wait until something is happening — for example some element is appearing or URL is changing. It is very common in angular.js application to wait until some element appearing. This is the way a user know that the action that he made actually done something.

Good indication is using browser wait and then use protractor.ExpectedConditions. We should use it only when the URL change or the state change. We don’t have to use it if page is not being replaced.

The syntax is quite simple browser wait and the EC, for example presenceOf:

browser.wait(EC.presenceOf(myPageObject.securityManagementLink))

Here the test will not move on until the element myPageObject.securityManagementLink will be present. Lovely, isn’t it?

The problem with browser wait is that the error massage that it print after failure is horrible. When browser wait is failing, we get something like that:

Error: Timeout — Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

Without ANY indication on what went wrong.

To get the browser wait to print nicer error, we should pass two more arguments: the first one the expected condition. The second one is the timeout and the third one is the error message that should be unique and informative as possible. Something like that:

browser.wait(EC.presenceOf(myPageObject.securityManagementLink), browser.params.timeout.ECstandards, 'securityManagementLink is not there');

#7 Use good reporter to report on errors and success

Don't settle for the regular reporter. The stack of error can be useful, but it is intimidating to people that are not code expert. There are a lot of good reporters that actually show the errors in more graphical way.

The best node.js module for this mission is https://www.npmjs.com/package/protractor-jasmine2-screenshot-reporter module. It will output an HTML report and also a screenshot to allow you to easily detect the problem:

Conclusion

Modern applications must have end to end testing and protractor\Selenium is one of the best tools out there. For angular and for other frameworks as well. But end to end testing can become kind of hell if your test code has problems. Using those tips will mitigate your pains in the tests creation and maintaining.

To view or add a comment, sign in

More articles by Ran Bar-Zik

Others also viewed

Explore content categories