Meet Protractor
A speed date with end to end testing
What is Protractor?
- Wraps WebDriver/Selenium
- Utilizes Jasmine, Mocha or Cucumber
- Makes hooking everything together easier
- Replaces Karma for Angular e2e testing
- Made for Angular, but not required
An end to end testing framework of frameworks
Why do we need e2e testing?
- Running real interactions in a real browser in a real deployment scenario with real* data
- Validate components actually work together
- Write a single test that runs against local and prod
- Meaning you know they behave the same
Do what the users do
* The realness of the data obviously depends on the enviornment
Doesn't e2e testing suck?
- Selenium Grids for faster parallel testing
- You already know the syntax
(Jasmine, Mocha, etc) - Better browser drivers
- TheIntern.io? (Maybe not the best choice for Angular sites)
Yes. But it's gotten better.
Why Protractor?
You could use WebDriver JS, but…
For Angular sites
- DOM access helpers
- By: models, bindings, ng-options, ng-repeat items
- waitForAngular
- Page load
- Binding updates
- DOM access helpers
- By: Button text (incl partial),
cssContainingText
- By: Button text (incl partial),
-
Custom locators
- e.g. Handlebars
- Globals: element, by, $, $$
For non-Angular sites
- Protractor config helps simply a lot of setup.
- Jasmine 1.3 didn't easily support async tests.
Not to mention...
Additional globals
The protractor global wraps the webdriver namespace.
elem.sendKeys(protractor.Key.ENTER);
browser.driver.manage().deleteAllCookies();
The browser global extends / contains webdriver.WebDriver. The raw driver is accessible through browser.driver.
Comparison
Protractor vs WebDriverJS
describe('angularjs demo page', function() {
it('should add one and two', function() {
//Global browser wraps already built WD
browser.get('http://bit.ly/1tUzecs');
//Global 'by' means shorter syntax
element(by.model('first')).sendKeys(1);
element(by.id('gobutton')).click();
//Global 'expect' unpacks Promises
expect(element(by.binding('latest'))
.getText()).toEqual('3');
});
});
var assert = require('assert'),
test = require('selenium-webdriver/testing'),
wd = require('selenium-webdriver');
test.describe('Google Search', function() {
test.it('should work', function() {
var driver = new wd.Builder()
.withCapabilities(
wd.Capabilities.chrome())
.build(); //Manual WD build
driver.get('http://www.google.com');
driver.findElement(wd.By.name('q'))
.sendKeys('foo');
driver.findElement(wd.By.name('btnG'))
.click();
driver.getTitle().then( function(title) {
assert.equal(title, 'foo - Google');
});
driver.quit(); //Note manual quit
});
});
Async Commands
WebDriverJS Control Flow + Protractor upgrades
To avoid callback hell, WebDriver uses Control Flow to queue commands.
element.click()
element.sendKeys('foo')
//is the same as
element.click().then(function() {element.sendKeys('foo') })
//Jasmine expect has been modified (jasminewd) to unwrap promises
expect(browser.getTitle()).toEqual('Protractor ');
Protractor uses this same system to it's advantage.
What you get out of the box
- Jasmine (1.3) Other frameworks supported, but not Jasmine 2.0. Soon to be Jasmine 2.x after Protractor 1.5
- WebDriver Manager Install & Update Selenium Servers and browser drivers
- Selenium Standalone Server So you don't need to run a remote server
- Chrome Driver Get testing right away. IE is a short command away
- elementexplorer.js Debugging element locators interactively
node_modules/protractor/[bin|selenium]
Protractor
Selenium Server
Web Server
localhost
localhost
localhost
CI server
Where does everything run?
Dev, QA, Stage, Prod
Another way to look at it
Source: "Testing AngularJS apps with Protractor"
Test
Server
Browser
Or
Getting Started
npm install -g protractor
describe('angularjs homepage', function() {
it('should have a title', function() {
browser.get('http://juliemr.github.io/protractor-demo/');
expect(browser.getTitle()).toEqual('Super Calculator');
});
});
exports.config = {
directConnect: true,
specs: ['spec.js']
}
Write a test spec //spec.js
Install with npm
Configure your setup //protractor.config.js
Text
Text
protractor protractor.config.js
Text
Run
Configuration
- shardTestFiles
files for capability will run in parallel - onPrepare
Setup Jasmine reporters,
Screen size, take screenshots - params
Global vars avail in all tests
params: { user:'foo', pwd:'bar' }
Highlights
Seriously. The reference config is great.
Okay, maybe a little more down below
Config for non-Angular apps
//config.js
onPrepare: function(){
browser.ignoreSynchronization = true;
}
//Otherwise in your specs
beforeEach(function() {
return browser.ignoreSynchronization = true;
});
//config.js
onPrepare: function(){
global.isAngularSite = function(flag){
browser.ignoreSynchronization = !flag;
};
}
//*.spec.js
beforeEach(function() {
isAngularSite(false);
});
Text
//config.js
onPrepare: function(){
browser.ignoreSynchronization = true;
}
//Otherwise in your specs
beforeEach(function() {
return browser.ignoreSynchronization = true;
});
Basic setup
Gettin' fancy
Sharing Config
//base.config.js
exports.config = {
baseUrl: http://nebraskajs.com/
multiCapabilities: [{"browserName": "internet explorer"},{'browserName': 'chrome'}]
}
//local.chromeOnly.config.js
exports.config = (function(){
var r = require('./protractor.base.config.js'); //Use require to pull in your base config
var tmpConfig = r.config;
tmpConfig.chromeOnly = true;
tmpConfig.chromeDriver = './node_modules/protractor/selenium/chromedriver';
tmpConfig.baseUrl = 'https://localhost:3000/';
//Use capabilities instead of multiCapabilities for grunt reasons
tmpConfig.capabilities = {
'browserName': 'chrome',
'chromeOptions': { 'args': ['show-fps-counter=true'] }
};
/* Null or clear out fields that might conflict with the above statements */
tmpConfig.multiCapabilities = [];
return tmpConfig;
})();
onPrepare is your friend
//Just to give you some ideas
onPrepare: function () {
var jasmineReporters = require('jasmine-reporters');
var fs = require('fs-extra');
browser.driver.manage().window().setSize(1024, 768);
browser.manage().timeouts().pageLoadTimeout(5000);
browser.manage().timeouts().implicitlyWait(3000);
// The IE driver doesn't like get('#/foo')
// This allows us to call getRoute('#/foo');
global.getRoute = function(route) {
return browser.get('/appContext/' + route);
};
var dir = 'out/tests/protractor/xmloutput';
fs.ensureDir(dir, function (err) {
if (err) {
console.log(err);
}
//dir has now been created, including the directory it is to be placed in
});
var capsPromise = browser.getCapabilities();
capsPromise.then(function (caps) {
var browserName = caps.caps_.browserName.toUpperCase();
var browserVersion = caps.caps_.version;
var prePendStr = browserName + '-' + browserVersion + '-';
//Do something cool
});
jasmine.getEnv().addReporter(new jasmine.ConsoleReporter());
/**
* NOTE: Changing versions of protractor or jasmine might require this to change.
* https://github.com/larrymyers/jasmine-reporters#protractor
*/
jasmine.getEnv().addReporter(
new jasmine.JUnitXmlReporter(dir, true, true)
);
}
Grunt + Protractor
-
Avoid multiple config files
grunt protractor:full:prod:ie
- Cannot override multiCapabilities
-
keepAlive: false //Needed for Jenkins
Overriding your config file
Page Objects
- Separates how something is done vs what it does
- Reduces code duplication
- Makes your tests more readable
An e2e testing best practice
Example Page Object
var AngularDemoPage = function() {
this.firstInput = element(by.model('first'));
this.secondInput = element(by.model('second'));
this.goButton = element(by.id('gobutton'));
this.latest = element(by.binding('latest'));
this.get = function() {
browser.get('http://juliemr.github.io/protractor-demo/');
};
this.setInputs = function(val1, val2) {
this.firstInput.sendKeys(val1);
this.secondInput.sendKeys(val2);
}
this.clickGoButton = function() {
return this.goButton.click();
}
/** @return Promise resolves answer value **/
this.getLatestText = function() {
return this.latest.getText();
}
};
module.exports = AngularDemoPage;
IDE autocomplete based on JSDocs makes Page Objects even better.
Testing with Page Objects
var AngularDemoPage = require('./demo.po.js');
var errorUtils = require('./../common/errors.util.js');
describe('angularjs demo page', function() {
it('should add one and two', function() {
AngularDemoPage.get();
AngularDemoPage.setInputs(1,2);
AngularDemoPage.clickGoButton();
expect(AngularDemoPage.getLatestText()).toEqual('3');
expect(errorUtils.hasErrors()).toBe(false); //Global page utils
});
});
Project Structure
|-- e2e/
| |-- components/
| | |-- datepicker.component.js
| |-- homepage/
| | |-- homepage.po.js
| | |-- *.spec.js
| |-- profile/
| | |-- profile.po.js
| | |-- *.spec.js
|-- e2e/
| |-- components/
| | |-- datepicker.component.js
| |-- pages/
| | |-- homepage.page.js
| | |-- profile.page.js
| |-- specs/
| | |-- homepage/
| | | |-- hompage.login.spec.js
| | |-- profile/
| | | |-- profile.social.spec.js
| | | |-- profile.pwdChange.spec.js
| | |-- *.spec.js
Pick one and stick with it
But keep your specs grouped by area. It helps in defining test suites.
Nitty Gritty of locators
Locator <= by.*(string)
ElementFinder <= element[.all](locator)
Promise <= foundElem.takeSomeAction()
An ElementFinder will not contact the browser until an action method has been called. Since all actions are asynchronous, all action methods return a promise.
element(by.css('some-css')).element(by.tagName('tag-within-css')).click();
element.all(by.css('some-css')).first().element(by.tagName('tag-within-css'));
ElementFinders can be chained
Just instructions until you pull the trigger
Debugging
-
Wait vs Sleep vs Debugger vs Pause
- Wait - Loop wait until a condition is true
- Sleep - Make the driver sleep for x millis
- Debugger - Pauses test & injects helper functions into browser
- Pause (BETA) - Inject protractor debugger in the control flow
- This is currently consider the best option
- No support (yet) for getting browser console
-
Docs (kinda) cover...
- WebStorm / IntelliJ debugging
- Might need to use computer name instead of localhost
- elementexplorer.js
- Taking screenshots on test failures
- WebStorm / IntelliJ debugging
The thing that might drive you mad
GOTCHAS
- Don't use ptor = protractor.getInstance()
- use 'browser' which is already global
- Don't use chromeOnly in config
- use directConnect, see docs
- WebDriverJS promises may not be the promises you're used to.
- The same goes for assertions.
- The Protractor API docs omit the WebDriverJS
functionality available through 'protractor' global - Manual bootstrap and $timeout
- Oh, and timeouts
- Protractor dictates which version of Jasmine you use
- sendKeys is not setValue - remember to clear()
What still sucks
- Still tough to debug
- "But WHY did my test fail?"
- On screen visibility - "Element not clickable"
- Driver differences (See leadfoot)
- No speed throttle
- Keeping on top of changes/failures
e2e testing - better, but not awesome
Still true
Learn More
Other Cool Stuff
Testability API - A bit of explaination
var testability = angular.getTestability(document.body);
testability.allowAnimations(false); //Faster tests
//Get notified when pending async ops known to Angular have been completed.
testability.whenStable(myCallback); //This is what protractor expect() uses
$compileProvider.debugInfoEnabled(false) //prod - significant performance boost
Thank You
https://www.linkedin.com/in/derekeskens
https://plus.google.com/+DerekEskens
These slides available on slides.com
Demo code on Github
Laptop designed by B. Agustín Amenábar Larraín from the Noun Project
Server designed by aLf from the Noun Project
Server designed by Norbert Kucsera from the Noun Project
Website designed by Max Miner from the Noun Project
Banana Peel designed by Liliane Lass Erbe from the Noun Project
Ramon Victor - Protractor for AngularJS
Credits
The Noun Project for kick ass icons
ng-newsletter - Practical End-to-End Testing with Protractor
Thiago Felix - Using Page Objects to Overcome Protractor's Shortcomings
Meet Protractor
By Derek Eskens
Meet Protractor
- 6,306