AngularJS E2E Testing with Protractor


In continuation to AngularJS series, today we'll discuss e2e or end-to-end testing of AngularJS applications. If you've been following the blog for a while, you must have noticed my numerous stressing the importance of unit testing using Jasmine and Karma and automating JavaScript testing with Grunt.js. The only thing left behind was e2e testing, of which we would talk today using Protractor for AngularJS applications.

What is E2E Testing?


End-to-end testing is a methodology used to test, whether the flow of an application is performing as designed from start to finish. The purpose of carrying out end-to-end tests is to identify system dependencies and to ensure that the right information is passed between various system components and systems.

In contrast to unit testing, which verifies the correct behaviour of various components separately, end-to-end testing verifies the entire flow of the application. From front end development perspective, it will be checking, whether JavaScript logic is reflected in the UI components. Good thing about Protractor is that we can write our end-to-end specs using Jasmine, so no knowledge of additional framework is needed.

angular-seed


To demonstrate the methodology, I'll be using angular-seed project, actually the whole article will be based on this repository. This project is an application skeleton for a typical AngularJS web app. You can use it to quickly bootstrap your angular webapp projects and dev environment for these projects. Installing the application is no-brainer, just follow the instructions in the repository - they are quite detailed. The reason I've chosen the seed project, was it had already had preconfigured Jasmine unit tests and e2e protractor tests in place. What is left is to understand the code :)

Unit testing with Karma


End-to-end testing doesn't replace the good old unit testing. It merely completes it to provide a comprehensive testing tookit. Let's take a look at Karma configuration file, karma.conf.js:
module.exports = function(config){
  config.set({

    basePath : './',

    files : [
      'app/bower_components/angular/angular.js',
      'app/bower_components/angular-route/angular-route.js',
      'app/bower_components/angular-mocks/angular-mocks.js',
      'app/components/**/*.js',
      'app/view*/**/*.js'
    ],

    autoWatch : true,

    frameworks: ['jasmine'],

    browsers : ['Chrome'],

    plugins : [
            'karma-chrome-launcher',
            'karma-firefox-launcher',
            'karma-jasmine',
            'karma-junit-reporter'
            ],

    junitReporter : {
      outputFile: 'test_out/unit.xml',
      suite: 'unit'
    }

  });
};
There are two interesting things about it. One is the integration with JUnit reporter, which reports test results in JUnit xml format. It than can be parsed programmatically and used for various DevOps purposes. For it however to work, you'll have to add the following line, indicating the usage of the reporter:
reporters: ['progress', 'junit']
The second thing is including angular-mocks.js file. It contains supporting functions for testing AngularJS application. Let's take a look at spec defined in version_test.js and see them in action.
'use strict';

describe('myApp.version module', function() {
  beforeEach(module('myApp.version'));

  describe('version service', function() {
    it('should return current version', inject(function(version) {
      expect(version).toEqual('0.1');
    }));
  });
});
We can see here the usage of functions module and inject. Both work in pair. The former registers a module configuration code by collecting the configuration information, which will be used when the injector is created by inject function. The latter wraps a function into an injectable function. The inject() creates new instance of $injector per test, which is then used for resolving references. You can read more about these functions in the respective documentation pages. You can also read a great article about Angular and Jamine here. The unit tests are run, as usual, using Karma command:
karma start karma.conf.js

End-to-end testing with Protractor


Protractor is a Node.js program built on top of WebDriverJS, which is Node.js runner, similar to node-jasmine, of which we've talked in Jasmine and Node.js article. Installing the driver is easy using the npm:
npm install -g selenium-webdriver
Then we need to set-up the selenium environment by running the following command:
npm run webdriver-manager
The interesting thing about this command is that it is run through the npm. The executed commands can be found in package.json file under scripts section.
"scripts": {
    "postinstall": "bower install",

    "prestart": "npm install",
    "start": "http-server -a localhost -p 8000 -c-1",

    "pretest": "npm install",
    "test": "karma start karma.conf.js",
    "test-single-run": "karma start karma.conf.js  --single-run",

    "preupdate-webdriver": "npm install",
    "update-webdriver": "webdriver-manager update",

    "preprotractor": "npm run update-webdriver",
    "protractor": "protractor e2e-tests/protractor.conf.js"
  }
So executing the webdriver-manager command, will actually execute webdriver-manager update. However since we have a pre prefix followed by the same name on another section, preupdate-webdriver, this script will be executed first - npm install. As you see configuring scripts through package file, allows us a lot of flexibility ensuring everything is run in the desired order.

Once everything is in place, let's start our e2e testing by typing the following command:
npm run protractor
Anddddd, it doesn't work - of course it won't :) So what is the problem:
....
protractor e2e-tests/protractor.conf.js

Starting selenium standalone server...
Selenium standalone server started at http://10.0.0.5:36333/wd/hub

/home/victor/git/angular-seed/node_modules/protractor/node_modules/selenium-
webdriver/lib/webdriver/promise.js:1640
      var result = fn();
                   ^
Error: Angular could not be found on the page http://localhost:8000/app/index.html :
retries looking for angular exceeded
From looking at the log we see that the webdriver is up and running on port 36333 and Protractor tries to fetch the page from port 8000. Is this the problem? As we can see Protractor runs according to configuration file e2e-tests/protractor.conf.js. Let's have a look at it:
exports.config = {
  allScriptsTimeout: 11000,

  specs: [
    '*.js'
  ],

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

  baseUrl: 'http://localhost:8000/app/',

  framework: 'jasmine',

  jasmineNodeOpts: {
    defaultTimeoutInterval: 30000
  }
};
Very similar to Karma config, isn't it? Run the specs written in Jasmine using Chrome on localhost:8000. But what is 8000? If we put here the port of our WebDriver, 36333, it won't help either, since WebDriver runs the Protractor tests and not the page itself. So the solution is pretty straight forward - configure web server on port 8000 to serve our app. Any server. Apache, Jetty or IIS God forbid, what ever is close to your heart. Rerunning the previous command will result some flickering on the page and console will report the passed tests. The tests are configured in scenarios.js file. I'll show you just one of them:
describe('view1', function() {

  beforeEach(function() {
    browser.get('index.html#/view1');
  });


  it('should render view1 when user navigates to /view1',
    function() {
    expect(element.all(by.css('[ng-view] p')).first()
      .getText()).toMatch(/partial for view 1/);
  });
})
Pay attention that instead of testing the internal logic of application, it rather tests the end result displayed to the user. That is, take the text of item retrieved by css rule, [ng-view] p, and test if it matches the string partial for view 1. That why it's called e2e testing.

Hope you found this article useful and would try to Protractor in your own projects. Next time we'll discuss Protractor usage with non AngularJS sites and also compare it to additional utility called CasperJS.

Comments

Popular posts from this blog

CAP Theorem and blockchain

Length extension attack

Contract upgrade anti-patterns