Get Hands-on with Protractor in 3 Steps
Theory only helps till a point with test automation (as with a lot of development practices). So in this article we're going to follow a practical approach to use Protractor. Following these 3 steps below, will give you sufficient hands-on practice to proceed further with Protractor.
Step 1 - Installing
Through a simple command npm install protractor -g, Protractor will be installed on your machine and ready to be run using the protractor command. However, executing this command will lead to an error message, as it is necessary execute commands in Protractor with a parameter. The parameter required by Protractor is the path to the Protractor config file, because to execute tests it is needed to have a config file to guide Protractor on how it should be run.
Step 2 - Setting up
There are many parameters that allow you to setup Protractor. Here are a few that are the most important in my opinion:
- SeleniumAddress: This allows you provide a URL to the Selenium Server that Protractor will use to execute tests. In this case Selenium Server must be previously started to be able to run tests on Protractor.
- SeleniumServerJar: This allows you provide the file path of the SeleniumServer.jar file. This parameter will be used by Protractor to control the Selenium life cycle. That way it is not necessary to start and stop the Selenium Server to run Protractor.
- SauceUser and SauceKey: When this parameter is used, Protractor will ignore the SeleniumServerJar parameter and will run tests against the Selenium Server in SauceLabs.
- Specs: An array of test files can be sent through the specs parameter for Protractor to execute. The path of the test files must be relative to the config file.
- seleniumArgs: This allows you to pass a parameter to Selenium if SeleniumServerJar is being used.
- capabilities: Parameters also can be passed to the WebDriver using the capabilities parameter. It should contain the browser against which Protractor should run the tests.
- baseURL: A default URL may be passed to Protractor through the baseURL parameter. That way all calls by Protractor to the browser will use that URL.
- framework: This can be used to set the test framework and assertions that should be used by Protractor. There are 3 options currently for this parameter: Jasmine, Cucumber and Mocha.
- allScriptsTimeout: To set up a timeout for each test executed on Protractor use this parameter with a number value in milliseconds.
All of the parameters are encapsulated in a node object which should be named "config". That way Protractor will identify those parameters inside that object. Below is an example of a Protractor configuration file named config.js
exports.config = { seleniumServerJar: './node_modules/protractor/selenium/selenium-server-standalone-2.39.0.jar', specs: [ 'tests/hello_world.js' ],
seleniumArgs: ['-browserTimeout=60'], 'browserName': 'chrome' }, baseUrl: 'http://localhost:8000', allScriptsTimeout: 30000 };
In this example the parameter seleniumServerJar is used to start the Selenium Server through Protractor. The folder tests will be executed. The file that contains all of the tests is hello_world.js and it is inside the tests folder. These tests are going to run against the Chrome browser due the capabilities parameter that is setup with the browserName attribute as 'chrome'. The timeout to run each test is 30 seconds.
To run the config file, simply run the command protractor passing config.js as the parameter. Protractor will run it following the instructions passed in the config file. However, we will get the following error message, as the hello_world.js file doesn't exist yet.
/usr/local/lib/node_modules/protractor/lib/cli.js:91 throw new Error('Test file ' + specs[i] + ' did not match any files.'); Error: Test file tests/hello_world.js did not match any files. at run (/usr/local/lib/node_modules/protractor/lib/cli.js:91:13) at Object. (/usr/local/lib/node_modules/protractor/lib/cli.js:265:1) at Module._compile (module.js:456:26) at Object.Module._extensions..js (module.js:474:10) at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12) at Function.Module.runMain (module.js:497:10) at startup (node.js:119:16) at node.js:901:3
So let’s create these tests to run on Protractor.
Step 3 - Creating tests
As mentioned in this introduction, Protractor runs on top of Selenium and because of that it provides all of the benefits Selenium already provides. However what makes Protractor the best test automation framework for AngularJS apps is the customization that can be done to Protractor directives.
The commands customized by Protractor aim to catch the elements of the application’s interface through AngularJS directives. Thus as you learn Protractor, you automatically learn about AngularJS behaviour and its concepts to render the UI. The converse also holds true - if you know AngularJS, it is easy to learn to use Protractor.
AngularJS uses some specific techniques to manipulate the Document Object Model (DOM), by inserting or extracting information from the HTML. For instance, ng-model is used to input data, binding is used to show data in the DOM and ng-repeat is used to show the information from the HTML of the javascript list in your AngularJS code.
To capture an element in the UI that contains an ng-model for example, Protractor provides the command by.model("name"). Using this command the framework gets a WebElement that contains the ng-model directive with the value "name"
dir="ltr"><div> dir="ltr"><label>Name</label> dir="ltr"><input type="text" ng-model="name"> dir="ltr"><label>Address</label> dir="ltr"><input type="text" ng-model="address"> </div>
Given the example above, when the command element(by.model("name")) is run, Protractor returns a WebElement as below:
<input type="text" ng-model="name">
To capture the elements that AngularJS uses to bind the information, the command is a little bit different, but the concept is the same as the ng-model one above. For example:
dir="ltr"><div> dir="ltr"> <label>{{name}}</label> dir="ltr"> <label>{{address}}</label> dir="ltr"> <label>{{postalCode}}</label> </div>
Above is the code that we use to bind information through AngularjS. Executing the command element(by.binding(address)) with Protractor returns the WebElement below:
<label>{{address}}</label>
When using ng-repeat, Protractor provides the command by.repeater("student in students") which returns an array of WebElements. This command allows you chain other functions like row and column to make your search more specific.
On chaining the row command, you receive an element with information from the object of the position passed by the parameter in the row command. For example by.repeater("student in students").row(0) returns the first element from the students list, because javascripts starts with an array of 0.
If you want to find a specific element inside a repeater structure, chain the column command with the name of the attribute that it is doing the binding in the HTML. Protractor will return the WebElement for which the binding is written. For example:
<div ng-repeat="student in students"><br> <span>{{student.name}}</span><br> <span>Score {{student.score}}</span><br> <input type="text" ng-model="student.score"/><br> </div>
The code above is the HTML structure created to run a AngularJS app. Given that our students list is an array which contain the students John, Peter and Paul, the HTML generated by AngularJS will be the following:
<div ng-repeat="student in students"><br> <span>John</span><br> <span>Score 8</span><br> <input type="text" ng-model="student.score"/><br> </div><br> <div ng-repeat="student in students"><br> <span>Peter</span><br> <span>Score 9</span><br> <input type="text" ng-model="student.score"/><br> </div><br> <div ng-repeat="student in students"><br> <span>Paul</span><br> <span>Score 7</span><br> <input type="text" ng-model="student.score"/><br> </div>
To understand how the command by.repeater works, we will use element(by.repeater("student in students")). Protractor will return the entire HTML structure shown above.
Running element(by.repeater("student in students").row(1)) will return HTML structure below:
<div ng-repeat="student in students"> <span>Peter</span> <span>Score 9</span> <input type="text" ng-model="student.score"/> </div>
In more specific cases to find a final element, run the element(by.repeater("student in students").row(1).column("score")) command. Protractor will return only the tag span, as below:
<span>Score 9</span>
Since we already know how to use some Protractor commands, let’s create tests to demonstrate how to use them. We’ll use the example on the AngularJS website (http://angularjs.org/) as shown below:
<html ng-app> <head> </head> <body> <div> <label>Name:</label> <input type="text" ng-model="yourName" placeholder="Enter a name here"> <hr> <h1>Hello {{yourName}}!</h1> </div> </body> </html>
We will create 2 tests:
- In the first one we're going to verify if the initial text inside tag h1 is "Hello !", because there is no value in the yourName attribute yet.
- After that we will use "Protractor" as the input to represent the yourName attribute and verify again that the text inside tag h1 contains the value "Hello Protractor!". The command sendKeys("Protractor") is a WebElement method to type values inside a text field.
describe('Hello World form', function() { it('display initial text', function() { browser.get(''); expect(element(by.binding('yourName')).getText()).toEqual("Hello !"); }); it('greets Protractor',function(){ browser.get(''); element(by.model('yourName')).sendKeys("Protractor"); expect(element(by.binding('yourName')).getText()).toEqual("Hello Protractor!"); }); });
Let’s include these tests in a hello_world.js file and use it as our test suite.
When we run Protractor now, we will not get an error message as earlier. It will run the tests inside the hello_world.js file and show the status of each test run.
Finished in 2.663 seconds 2 tests, 2 assertions, 0 failures
Voila! Your tests now run fine. You can add as many tests as you need to your test suite, integrate them using a CI tool and run them continuously.
Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.