Protractor for AngularJS

Writing end-to-end tests has never been so fun

@ramonvictor

Table of contents

Why is testing so important?

Testing is about gaining confidence that your code does what you think it should do

@juliemr

Whats the idea behind E2E testing?

  • How would the users see my application?
  • Is my backend communicating with my frontend?
  • Can I release this code?
  • It does NOT replace Unit Testing!

The dark side of E2E testing

  • It needs a specific running environment
  • It's hard to write
  • It's difficult to debug
  • It's hard to keep the tests up-to-date

Protractor is built on top of WebDriverJS

Testing system (NodeJS, Java, etc)

|

Webdriver (a.k.a. Selenium)

|

Your AngularJS App

Protractor Architecture

Reference: Testing AngularJS apps with Protractor

Install

  1. Download Node.JS
  2. sudo npm install protractor -g
  3. sudo webdriver-manager update

Setup a conf.js file

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',

  capabilities: {
    'browserName': 'chrome'
  },

  specs: ['example-spec.js'],

  jasmineNodeOpts: {
    showColors: true
  }
};

Write your tests using Jasmine and WebdriverJS

describe('by model', function() {
   it('should find an element by text input model', function() {
     var username = element(by.model('username'));
     var name = element(by.binding('username'));
   
     username.clear();
     expect(name.getText()).toEqual('');
   
     username.sendKeys('Jane Doe');
     expect(name.getText()).toEqual('Jane Doe');
   });
});

Protractor global variables

  • browser: browser.get()
  • element and by: element(by.model('yourName'))
  • protractor: protractor.Key

Basic example

// example-spec.js
describe('angularjs homepage', function() {
  it('should greet the named user', function() {
    browser.get('http://www.angularjs.org');

    element(by.model('yourName')).sendKeys('Julie');

    var greeting = element(by.binding('yourName'));

    expect(greeting.getText()).toEqual('Hello Julie!');
  });
});

Let's run it?

First things first, open the terminal and start the webdriver server:

webdriver-manager start

After that, you can run Protractor in another terminal by typing:

protractor test/e2e/config.js // this is the relative path to your config.js file

Searching for elements on the page

element() vs element.all()

Single element

element( by.binding('appName') );

Collection of elements

// clicks the 3rd element
element.all( by.css('[ng-click="openPage()"]') ).get(2).click();

by.binding

In your test

element( by.binding('myModel') );

In your application

<span ng-bind="myModel"></span>
<!-- or -->
<span>{{myModel}}</span>

by.model

In your test

element( by.model('myModel') );

In your application

<input ng-model="myModel" />

by.repeater

In your test

element( by.repeater('user in users').row(0).column('name') );

In your application

<ul>
  <li ng-repeat="user in users">
      <span>{{user.name}}</span>
  </li>
</ul>

by.css

In your test

element( by.css('[ng-click="sendMail()"]') );

In your application

<button ng-click="sendMail()">Send mail!</button>

Find out more in Protractor API

Searching elements best practices

  • Take advantage of AngularJS attributes using by.model, by.repeater, etc
  • Avoid using potential CSS attributes, mainly IDs and Classes.

Executing events

.click()

In your test

element( by.css('[ng-click="submit()"]') ).click();

In your application

<button ng-click="submit()"><button>

On Enter Press

In your test

element( by.model('commentText') ).sendKeys("Hi!", protractor.Key.ENTER);

In your application

<textarea ng-model="commentText"><textarea>

Debugging using elementexplorer

  1. Start your webdriver server:
    webdriver-manager start
  2. Run:
    /usr/local/lib/node_modules/protractor/bin/elementexplorer.js http://angularjs.org
  3. Press 'tab' and play with any element locator.

Maintanable Tests

Best practices to organize your tests

The big picture

  • Page Objects - These are the js files where you map the elements and write the functions to perform actions;
  • Exports and Require - This is how you connect your Page Objects to your Test Specs;
  • Test specs - These are the js files where you write your tests using jasmine syntax.

Tests directory structure

projectfolder/
  |-- css/
  |-- js/
  |-- img/
  |-- tests/    
    |-- unit/           
    |-- e2e/  
    |    |-- homepage/
    |    |     |-- homepage.po.js
    |    |     |-- *.spec.js
    |    |-- profile/
    |    |     |-- profile.po.js
    |    |     |-- *.spec.js
    |    |-- config.js

Page Objects

var AngularHomepage = function() {
  this.nameInput = element(by.model('yourName'));
  this.greeting = element(by.binding('yourName'));

  this.get = function() {
    browser.get('http://www.angularjs.org');
  };

  this.setName = function(name) {
    this.nameInput.sendKeys(name);
  };
};

Node.JS exports and require

Your Page Object file
var AngularHomepage = function() {
  this.nameInput = element(by.model('yourName'));
  this.greeting = element(by.binding('yourName'));
  // ...
};
module.exports = AngularHomepage;
Your Test file
var AngularHomepage = require('./homepage.po.js');
describe('HomePage Tests', function(){
   var angularHomepage = new AngularHomepage();
   angularHomepage.nameInput.sendKeys('Rafael');
   //...
});

Go further!

Separate your tests in various test suites

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  capabilities: { 'browserName': 'chrome' },

  suites: {
    homepage: 'tests/e2e/homepage/**/*Spec.js',
    search: ['tests/e2e/contact_search/**/*Spec.js']
  },

  jasmineNodeOpts: { showColors: true }
};
Running specific suite of tests
protractor protractor.conf.js --suite homepage
				

Enable multiCapabilities

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',

  multiCapabilities: [
	{
	  'browserName' : 'chrome'
	}, 
	{
	  'browserName' : 'firefox'
	}
  ],

  specs: ['example-spec.js'],

  jasmineNodeOpts: {
    showColors: true
  }
};

Using onPrepare

Set window size before starting the tests

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',

  capabilities: {
    'browserName': 'chrome'
  },

  onPrepare: function() {
     browser.driver.manage().window().setSize(1600, 800);
  },
 
  jasmineNodeOpts: {
    showColors: true
  }
};

Using onPrepare

Export xml results of your Automated Suites

First, install jasmine-reporters:

npm install jasmine-reporters

And to keep xml results in Timestamp directories, install mkdirp package:

npm install mkdirp

Using onPrepare

Export xml results of your Automated Suites

//config.js
exports.config = {
  onPrepare: function() {
    var folderName = (new Date()).toString().split(' ').splice(1, 4).join(' ');
    var mkdirp = require('mkdirp');
    var newFolder = "./reports/" + folderName;
    require('jasmine-reporters');
    
    mkdirp(newFolder, function(err) {
      if (err) {
        console.error(err);
      } else {
        jasmine.getEnv().addReporter(new jasmine.JUnitXmlReporter(newFolder, true, true));
      }
    });
  },
};

Using params

Your config.js

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',

  capabilities: { 'browserName': 'chrome' },

  // This can be changed via the command line as:
  // --params.login.user 'ngrocks'
  params: {
    login: {
      user: 'protractor-br',
      password: '#ng123#' 
    }
  },
 
  jasmineNodeOpts: { showColors: true }
};

Using params

Your test

describe('login page', function() {

  var params = browser.params;

  it('should login successfully', function() {
    element( by.model('username') ).sendKeys( params.login.user );
    element( by.model('password') ).sendKeys( params.login.password );
    element( by.css('[ng-click="login()"]') ).click();
    expect( element(by.binding('username') ).getText() ).toEqual( params.login.user );
  });   

});

Using jasmineNodeOpts

exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',

  capabilities: { 'browserName': 'chrome' },

  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    isVerbose: true,
    includeStackTrace: true
  }
};

See a full list of available options for a config.js

"But, I want to use Protractor in a non-AngularJS app"

Sorry, you can't! :(

Just kidding, of course
you can! :)

You only need to access the webdriver instance by using browser.driver:


browser.driver.find(By.css('[data-ptor="submit-btn"]'));
					

It can be even more elegant

In your config.js

onPrepare: function(){
   global.dvr = browser.driver;
}

In your test

dvr.find(By.css('[data-ptor="submit-btn"]'));

Protractor waits for Angular to finish its work

Though you can tell it not to be that smart about your non-Angular app:

beforeEach(function() {
   return browser.ignoreSynchronization = true;
});

Let's make it more semantic?

In your config.js

onPrepare: function(){
   global.isAngularSite = function(flag){
      browser.ignoreSynchronization = !flag;
   };
}

In your test

beforeEach(function() {
   isAngularSite(false); // isAngularSite(true), if it's an Angular app!
});

Reference: Protractor - Testing Angular and Non-Angular Sites

Bonus

gulp-protractor-qa

It warns you on the fly whether all element() selectors could be found within your AngularJS view files.

Video screen demo of gulp-protractor-qa in action!

How to use gulp-protractor-qa?

Final thoughts

  • E2E testing is a complement to Unit testing
  • Avoid using CSS attributes
  • Write your tests with scalability in mind
  • Just have fun testing with Protractor

Learning Resources

Thank you :)

@ramonvictor

Thanks to @rafaelbattesti contributions!

Fork me on GitHub