Protractor and Page Objects: why you should use a design pattern in your tests
Protractor offers an excellent testing system, perfectly integrated with Angular, but as we have presented it in the previous article, it brings with it some evident problems.
The goal is to identify and avoid everything that would make test maintenance difficult: the growth of the project in size and complexity would actually make testing a cost in place of a resource and would irreparably lead to their abandon.
This article is the english translation from the original “Protractor e Page Objects: l’importanza dell’utilizzo di pattern nei test” also written by me for my corporate blog.
This article is also part of a series of publications about testing the frontend that will provide some concrete cases found in the daily development of functional tests of Angular 2 applications performed through Protractor. You can read the original Italian version here.
A working example of the angular base project including all tests that we will use as example is available at: https://github.com/mzuccaroli/angular-cli-tests-example, it is a simple angular2 project generated with angular CLI with the command “ng new”. For more information see the quickstart of an angular2 project.
Main problems of an unstructured approach to testing with protractor:
Let’s take as an example this test that logs in and checks for a welcome message:
In addition to reading difficulty of that makes it impossible to understand “on the fly” what the goal of the test (that will force us to waste time writing long documentation) is you can see the following problems:
- Lack of support for a DLS (Domain Specific Language): it is difficult to understand what is being tested, because the typical Protractor structures (element, by.binding, by.css, etc.) are not directly related to the features to be tested. Having tests using the same jargon as the application domain is very useful for helping those who have not developed them to understand the motivations behind the tests.
To give an example, it is much easier to understand the purpose of a code written like this: “loginPage.getUsernameInput()” instead of “element(by.css(‘#registration> div> div> form> div> input.username’) “
- Code duplication: it is a direct consequence of the previous point, if we need to click on a button several times to test a function, the code the code responsible for the selection of the button will be repeated every time, but mostly will be duplicated on multiple tests that incidentally have to click on the same button.
- High coupling: the logic of the tests is strictly bonded to the code and to the DOM selectors, marginal changes on the structure of the DOM or classes, which usually occur on the advanced phases of a project, could make necessary to rewrite all the tests. It is essential to maintain a low coupling and be flexible enough to be ready for change
Page Objects: a dedicated design pattern
A solution to these problems is the use of Page Objects aka PO, a design pattern introduced by the Selenium team, aimed at improving test maintenance and reducing code duplication. A Page Object is a class / object that serves as an interface to the application pages providing an abstraction of this towards the tests.
In the testing process we can then call the methods of the Page Object without worrying about the implementation of the page.
Following a change in the UI of the page or its implementation we will only have to update the PO code without having to deal with the tests that verify the functionality.
Applying this pattern solves the above problems:
- DLS support: all the selectors of the page or the Protractor’s typical grammars can be moved in the PO exposing only methods related to logic and functionality, with names that are relevant and easy to read as: “page.getLoginButton().click()”
- Code duplication: an external object that can be called by multiple tests avoids any duplication of code, allowing us also to automate duplicate parts: if we need to perform many tests as authenticated users in our application a simple “page.doUserLogin()” allows us to write once the code that automates the login.
- High coupling: this kind of function also allows us to implement decoupling, if the implementation of the login form changes we will have to modify only the “doUserLogin()” function without touching the tests code
Following this sample pattern the example test can be refactored as follows:
While all the logic linked to the DOM and the browser is moved to the new PO:
Note that, even in this ultra-basic test, if the text to be checked is moved from h1 to another type of html element or the page route changes from “/login” to “/home” only the PO should be updated while the test remains the same and the functional logic is in no way affected.