Two days to write my first Protractor tests on the ng-admin AngularJs module

Jérôme Macias
Jérôme MaciasDecember 12, 2014
#ng-admin#testing#tutorial

What is Protractor?

Protractor is the de facto standard end-to-end testing library built by the AngularJs team. It's a NodeJs program using WebDriver to control browsers, and Jasmine for the test syntax. Protractor works out-of-the-box for any AngularJs application using automatic bootstrapping with ng-app directive.

There is a complete documentation available on the AngularJs website.

Setting Up Protractor and WebDriverJS

To install Protractor, just launch the following command:

npm install protractor --save-dev && ./node_modules/protractor/bin/webdriver-manager update

Now, you need to build the Protractor configuration file.

I first tried to use the requirejs syntax as we usually do in ng-admin project, but I failed. Maybe I didn't have enough experience, but let's use the standard documented syntax.

exports.config = {
  specs: ["e2e/*.js"],
  baseUrl: "http://localhost:8000",
  maxSessions: 1,
  multiCapabilities: [{ browserName: "chrome" }],
};

You are now ready to use Protractor.

Writing The First Test

I wrote a very basic spec, just to ensure Protractor works on my app:

describe("ng-admin dashboard", function() {
  describe("Controller: MainCtrl", function() {
    it("should work", function() {
      browser.get(browser.baseUrl);
      expect(true).toBe(true);
    });
  });
});

This test simply loads the root application url in a browser, and makes an always true assumption.

Instrumenting With Grunt

Here are the required steps to run e2e tests on the ng-admin application:

  • Initialize configuration file from *-dist
  • Build application
  • Start JSON API sever
  • Start ng-admin application
  • Run Protractor tests

I used Grunt to automate these tasks. Here is the Grunt task I wrote for the first step:

copy: {
    config: {
        src: 'src/javascripts/config-dist.js',
        dest: 'src/javascripts/config.js',
        options: {
              process: function (content, srcpath) {
                  return content.replace(/@@backend_url/g, process.env.CI ? 'http://ng-admin.marmelab.com:8080/' : 'http://localhost:3000/');
                },
            },
        }
    }
}

I used grunt-contrib-copy which allows some basic replacements on file after moving it. It's very useful.

The 2 copies run concurrently with the new init task:

grunt.registerTask("init", ["copy:config"]);

At this step, we can execute Protractor if the ng-admin application and JSON API server have already been started by hand.

Automate The Starting of the ng-admin Server and JSON Rest API

I used grunt-contrib-connect for the server and grunt-json-server for the API. There is just one detail about using those tasks.

We already use the connect plugin with keepalive option set to true, to tell the task to wait forever. But in case of test automation, we need to exit at the end of script.

So keepalive must set to false for both tasks. But this option wasn't working for grunt-json-server. This task was developed recently, it's the rule in a young ecosystem like nodejs.

I submitted a pull request to fix this issue.

The second part of the configuration looks like:

connect: {
    server: {
        options: {
            port: 8000,
            base: '.',
            keepalive: false
        }
    }
},
json_server: {
    stub: {
        options: {
            port: 3000,
            db: 'src/javascripts/test/stub-server.json',
            keepalive: false
        }
    }
}

We can now launch tests on our local env with a new local-test task:

grunt.registerTask('test-local', ['json_server', 'init', 'build:dev', 'connect', 'protractor']);

Integration with Travis-CI and SauceLabs

We already use Travis for launching tests on the ng-admin github repository. SauceLabs provides another element in the Continuous Integration chain: real browser testing. SauceLabs is free for open source project: it's called "Open Sauce". SauceLabs have a good integration with Protractor and Travis, really good, with a complete documentation. The documentation for SauceLabs Travis integration is available here.

As a bonus, we can generate a secure encrypted credentials for Travis configuration. Ng-admin is a public open source project, so we need to take care to not reveal our secret key.

To enable SauceLabs in Travis, it's just few lines of configuration:

env:
  global:
    - secure: ...MQeyWU/0FWl4cX5fJiJsXqVtNJWzVjxi5gD9MJp7Ozr0880akopb7KdEb80zdlG1y...
    - secure: ...Sm2jRVShFu7Ri68Q895rEqj2wAEu4ZriMGWwJiONAsSILlxcKNluHjg7yLQoQXypj...
addons:
  sauce_connect: true

We can now easily use credentials in the Protractor configuration:

sauceUser: process.env.SAUCE_USERNAME,
sauceKey: process.env.SAUCE_ACCESS_KEY,

To be able to use the Sauce Connect tunnel with Travis, we need to configure Selenium WebDriver and specify a tunnel-identifier:

    multiCapabilities: [
        {
            browserName: 'chrome',
            'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER ? process.env.TRAVIS_JOB_NUMBER : null,
            name: 'ng-admin'
        }
    ],

Note that I also configured the Job name displayed on SauceLabs.

Last, we need to update the Grunt task test:

if (parseInt(process.env.TRAVIS_PULL_REQUEST, 10) > 0) {
  grunt.registerTask("test", ["karma"]);
} else {
  grunt.registerTask("test", ["karma", "init", "connect", "protractor"]);
}

We can't run SauceLabs on pull requests, because a pull request comes from a fork, and the SauceLabs credentials don't match with the fork repository slug.

First Push on Github

Tests failed!

Even with sauce_connect enabled on Travis, which allows to use a localhost instance of our app behind a firewall, our local application cannot fetch data from the local API.

I had to use the ng-admin demo API, but it's not really safe and data are not stable. After this modification, tests pass on Travis on the very first time but not on next commits.

After some investigations, I found an answer. The sauce_connect tunnel has some troubles to reach localhost, so we need to specify a named host.

I choose ngadmin as hostname, and thanks to Travis we can specify hosts on the addon section of the configuration file:

addons:
  hosts: ngadmin

We also need to change the baseUrl configuration of Protractor:

baseUrl: "http://" + (process.env.CI ? "ngadmin" : "localhost") + ":8000";

Another retry, another error: a timeout while waiting the AngularJs app was loaded.

To fix that, I was forced to increase the defaultTimeoutInterval of JasmineNode option in the Protractor configuration file:

jasmineNodeOpts: {
  defaultTimeoutInterval: 360000;
}

It now works, hurray :)

Adding More Tests

Protractor is a wrapper around WebDriverJS and the WebDriverJS API is based on promises. It was hard for me deal with it. The REPL for Protractor Locator helped me a lot.

To start an instance of the included REPL, just run the following command:

./node_modules/protractor/bin/elementexplorer.js http://localhost:8000

Thanks to this tool, it becomes faster to understand and write tests without running all the process again and again.

Here is my first tests suite:

describe("ng-admin", function() {
  describe("Dashboard", function() {
    it("should display a navigation menu linking to all entities", function() {
      browser.get(browser.baseUrl);

      $$(".nav li").then(function(items) {
        expect(items.length).toBe(3);
        expect(items[0].getText()).toBe("Posts");
        expect(items[1].getText()).toBe("Tags");
        expect(items[2].getText()).toBe("Comments");
      });
    });

    it("should display a panel for each entity with a list of recent items", function() {
      browser.get(browser.baseUrl);

      element
        .all(by.repeater("panel in dashboardController.panels"))
        .then(function(panels) {
          expect(panels.length).toBe(3);

          expect(
            panels[0]
              .all(by.css(".panel-heading"))
              .first()
              .getText()
          ).toBe("Recent posts");
          expect(
            panels[1]
              .all(by.css(".panel-heading"))
              .first()
              .getText()
          ).toBe("Last comments");
          expect(
            panels[2]
              .all(by.css(".panel-heading"))
              .first()
              .getText()
          ).toBe("Recent tags");
        });
    });
  });
});

But two days passed, and I had not enough time to write more tests, too bad. I hope that the bootstrap of Protractor over Travis was the hardest part, and that we can now easily add more tests when necessary.

Conclusion

It was a real pleasure to test Protractor and use Selenium server on command line to be able to test on a real browser. Protractor is a really good end-to-end testing tool.

I also learned so many things on Grunt and automation process in the NodeJs ecosystem.

There are still some details to focus on:

  • Find a way to use a local API for tests.
  • Set the job name for SauceLabs
  • Test on more browsers
  • Apply some best practices

All code has been merged to the ng-admin repository, you can browse it on the pull request.

Did you like this article? Share it!