In this blog you will learn some ultra useful tips to apply when creating automation framework based on Cucumber for JS with Protractor.
Why automate the software testing?
Testing is a repetitive activity through the whole software life development cycle (SLDC).
Regression testing for instance should be run regularly. Can you imagine running the same 100 tests again and again after every code changes? It’s boring. It’s also a waste of talent: testers enjoy exploratory testing, and this is where they bring the most value. With automation you can free up your QA team to do more exploratory work or write the test for new functionality.
Test automation also cuts the cost of testing. What test automation does is reducing manual testing effort by taking care of those tests that need to be run regularly. Once you set up your automation, those repetitive tests will run themselves. You just need to press play when you want them or set them up to run during the night and just check the results in the morning.
To be precise in calculating the cost of automation you should include the learning curve of used tools and technology for your QA team. For the proposed tools in this blog the learning curve cost is relatively low (of course it depends on the technical knowledge of your team).
What is an UI functional test?
UI functional tests (some of them can be End to End tests) are tests that simulate user actions. It means that the tests are supposed to actually emulate a real user, which 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. They do not miss anything and the QA team can focus their attention to create new regression tests.
Why Cucumber?
Definition from Wikipedia: Cucumber is a software tool used by computer programmers for testing other software. It runs automated acceptance tests written in a behavior-driven development (BDD) style. Central to the Cucumber BDD approach is its plain language parser called Gherkin.
This BDD syntax is great as it allows describing more complex scenarios, not just simple tests as unit tests for example. Every person in the team with sufficient product knowledge can contribute to get quality software product by writing test scenarios. When these scenarios are written in a smart way and are extensive enough to cover all application aspects, they can serve as a live functional documentation.
Here is a simple scenario in Cucumber:
Scenario: Login user Given I go to URL "target url" Scenario When I type "test_user" in the username_field And I type "task1234" in the password_field And click button_login Then the user should be logged
Cucumber also allows to organize suites of tests together with tags and hooks.
For my framework I use Cucumber JavaScript implementation in order to combine it with Protractor API.
Why Protractor?
Protractor is an end-to-end test framework for AngularJS applications. Protractor runs tests against your application running in a real browser in asynchronous way. Thus it takes care to synchronize the test steps with your application, so that they will be executed properly at the right time, only when the application is ready which is valuable advantage. All the actions (mouse clicks, keyboard typing, etc.) and all the checks / expectations actually return promises, that will be executed in the future. At the start of your tests, all of your test scripts are parsed and a chain of promises is created.
Automation tests should be treated as a code!
Protractor is very easy to use and create, but it can be hard to maintain and avoid false-positives. In order to facilitate the creation and maintenance of the tests, there are several rules:
#1 tests are node.js applications!
Protractor tests are basically 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
- Avoid callback hell by using events or promises.
- Use static code analysis tool (like eslint or istanbul) to ensure code conventions.
- Use strict mode to avoid common errors.
- Store your tests in the tests repository.
- Create readme file with sufficient information about configuring the running environment, and working with the tests.
#2 Include specific hard-coded data inside the config file
In this file you should put things like url of the web application under test, login credentials, browsers data, parameters like time out constants for general actions. In my case this is protractor.conf.js file which looks like that:
'use strict'; module.exports.config = { useAllAngular2AppRoots: true, // Before performing any action, Protractor waits until there are no pending // asynchronous tasks in your Angular application allScriptsTimeout: 120000, getPageTimeout: 10000, specs: [ 'e2e/features/**/*.feature' //path to your feature file ], //Cucumber throws uncaught exceptions and protractor fails the test suite immediately, hence this ignoreUncaughtExceptions: true, cucumberOpts: { // Require step definitions require: [ 'e2e/steps-definitions.js', 'e2e/utils/hooks.js', ], format: 'json:.tmp/results.json', profile: false, 'no-source': true, tags: '@acceptance', dryRun: true, defaultTimeout: 8000 }, multiCapabilities: [ { browserName: "chrome", } ], baseUrl: 'your app url', framework: 'custom', // Path relative to the current config file frameworkPath: require.resolve('protractor-cucumber-framework'), // Custom parameters can be specified here params: { // Path to file with all page objects pageObjects: require('./e2e/page-objects/page-index.js'), // Custom timeout to wait for elements on the page customTimeout: 8000, // Params for setting browser window width and height - can be also // changed via the command line as: --params.browserConfig.width 1024 browserConfig: { width: 1920, height: 1080 } }, }
Cucumber is no longer included by default for Protractor so you will pass in the custom option for your framework plus a few extras for the Cucumber framework itself.
#3 Use page objects
Page objects are Object Repository Design Pattern for creation of a bank of web elements you will use in the test scenarios. 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 loginlogout process.
Here is а working example of page object:
'use strict'; module.exports = (function () { let tasksPage = { dropdownArrow: 'span.fa.fa-fw.fa-caret-down.ui-clickable', selectedFilteredItem: '//div/p-dropdown/div/label[contains(text(),"text")]', Filters: '.material-icons.filter-object-type', filterHeader: '.filter-header', checkboxInProgress: '.checkbox-block>li>input', checkboxNotStarted: '.checkbox-block>li:nth-of-type(2)>input', dueText: '.task-date-warning', tabMyTasks: 'label.switch-field-left', tabGroupTasks: 'label.switch-field-right', iconHelp: 'i.icon.icon—xlarge.icon--action', //Top bar buttons buttonTopBar: 'button>span.ui-button-text.ui-clickable', //Dialog overlay selectors buttonOverlay: 'button.overlay__action', taskOverlay: '.overlay.overlay--visible', buttonConfirm: 'button.btn.btn--primary', }; return tasksPage; })();
#4 Use good CSS and XPath selectors
Selectors are the main ingredient of the tests to select elements for clicking, asserting or anything else. Protractor allows us a lot of selectors (called locators in protractor) described here.
Choosing a selector is a specific task and requires some skills. The long xpath is not a good practice, please take a look at this:
“/html/body/div/div/div[2]/div/div[3]/div/div[2]/div[2]/form/fieldset/ng-transclude/div[1]/div[1]/div[2]/div[1]/label”
You can use this instead:
“//label[text() = ‘Content Files’]”
This is the same element. Always try to use meaningful XPath or CSS selector that is easy to debug.
If you test Angular application you can use also ngmodel and bindings:
// Find an element with a certain ng-model. by.model('name') // Find an element bound to the given variable. by.binding('bindingname')
If you have web elements id-s, you can go with them too as they are unique identifiers.
#5 Assertions
Every test case should be designed with the proper assertion. Otherwise we will end up with false positive passing tests and many bugs in the product.
A good choise is Chai and Chai-As-Promised.
Here are two examples of assertion steps: the first step checks if the check box element defined with
const elmnt = composeLocator(page, elem);
is checked. If it is checked the step will pass, otherwise will fail with an assertion error. The second step asserts if an element has a specific string.
Then('{string}.{string} is checked', (page, elem) => { const elmnt = composeLocator(page, elem); return browser.wait(EC.elementToBeClickable(elmnt)) .then(() => expect(elmnt.isSelected()).to.eventually.equal(true)); }); Then('{string}.{string} has text {string}', function (page, elem, text, callback) { const elmnt = composeLocatorUsingText(page, elem, text); expect(elmnt.getText()).to.eventually.equal(text).and.notify(callback); });
where composeLocatorUsingText is implemented like this:
function getElmnt(locator) { let elmnt; if (locator[0] + locator[1] === '//') { elmnt = element(by.xpath(locator)); } else { elmnt = element(by.css(locator)); } return elmnt; } /** * Composes proper element locator by changing text in xpath locator * @param {string} page * @param {string} elem * @param {string} text * @returns {object} elmnt */ function composeLocatorUsingText(page, elem, text) { let locator = pageObjects[page][elem]; if (text && locator.indexOf('"text"') > -1) { locator = locator.replace('"text"', '"' + text + '"'); } return getElmnt(locator); }
#6 Use 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 experts. 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 this module. It will output an HTML report and also a screenshot to allow you to easily detect the problem:
Recap:
Now you learned about Protractor advantages, basic structures of both Protractor and Cucumber.js frameworks and how to build features that run seamlessly with both tools. Using Cucumber with Protractor gives you the opportunity to create tests that are readable for your team as well as build the documentation for your application at the same time.
What is your experience on the topic? I will be glad if you share it in the comments section below.