AngularJS Headless End to End Testing With Protractor and Selenium

FROM HERE

For those of you who are already somewhat familiar with Protractor and Selenium and want to skip right to the end, you might take a look at my Vagrant VM for headless browser testing in Ubuntu, and Chef cookbook for the same. These set up a standalone server that can be used to run end to end tests of AngularJS sites in Chrome, Firefox, or PhantomJS, and address some of the issues that you will run into along the way.

What is an End to End Test?

An end to end test runs against the front-end of a fully functional application. In the case of a web application, this implies driving a browser that loads pages, runs Javascript, interacts with the DOM, fills and submits forms, and so forth. The web application is served from a machine with a fully functional backend and suitably populated database. The setup is intended to mimic as closely as possible the live application, environment, and use cases.

You might compare this with unit testing, wherein a unit test runs against a small piece of functionality in isolation. Any data that might come from the rest of the application is mocked for the purposes of a unit test.

An AngularJS Test Philosophy

A fair way to approach design of an AngularJS application is to use a REST-like backend, push all of the business logic into services, and treat controllers as little more than glue holding together routes, directives, and the aforementioned services. When doing this, you will find that pretty much every piece of code worthy of a unit test ends up in a service and can be somewhat decoupled from $http requests. Thus you need only unit test the client-side business logic, and whether that involves the use of the mock $httpBackend or just ad-hoc construction of mock data is up to you. Personally I find that the latter is more easily set up and maintained.

The remaining test coverage of the codebase can be ensured with end to end tests. These by their nature will exercise directives, the glue controllers, and the functionality of the application when running against a known backend. A build and deployment system should incorporate the setup of a site database and server that is used for this purpose.

I find that a combination of more limited service-focused unit testing followed by a layer of end to end testing is more cost-effective than trying to push out to a high level of coverage for unit tests alone. Writing mock server responses for unit tests is exceedingly time-consuming, both to create the things and then to later maintain them: to my eyes it is better to use that time to set up a known database and backend and build end to end tests to run against it.

Karma and ngScenario: the Obsolete End to End Test Setup for AngularJS

The AngularJS documentation discusses the use of Karma and ngScenario to run end to end tests against a running web application server. The ngScenario framework provides a Selenium-like API for driving a browser and the odds are good that you are already using Karma for unit tests, so it seems like a simple evolution of existing work to start using it. That said, I have not been able to make ngScenario work to my satisfaction: local tests function but the same tests running against remote sites fail for deep reasons I have not put in the time to debug. Judging by what I’ve read I seem to be in a minority there, unfortunately, so it’s hard to say what it is that I am doing wrong.

Either way, this framework for end to end testing is now obsolete and is in the process of being replaced by Protractor. So don’t put any time into it.

Protractor, WebDriver, and Selenium

protractor-components

Selenium and WebDriver provide local and server APIs for driving a browser and manipulating and inspecting the DOM on loaded pages – and thus running tests against a site. Selenium, like most of the tools in this ecosystem, is presently evolving into a new configuration, but is reliable. A typical Selenium set up is:

  • A Selenium standalone server listens at a port for API commands.
  • Commands arrive indicating that Selenium should start a browser session.
  • Selenium loads a WebDriver implementation to control a particular browser.
  • The browser is started and pointed to a web site, usually on another server.

Typically test scripts run on server A and connect to Selenium on server B. Selenium fires up a browser on server B to connect to a web application running on server C. The test scripts on server A instruct Selenium on server B to drive the browser around the site served from server C, checking the state of the DOM and content in response to various actions.

It is perfectly possible, but painful, to write end to end tests for an AngularJS site using only Selenium tools. The challenge lies in determining when AngularJS is actually done with a given action, such as a change of view – this is somewhat more difficult than is the case for straightforward old-school AJAX operations. So Selenium test scripts for AngularJS tend to bloat with wait commands and checks.

Protractor is a Node.js framework that sits on top of the Selenium / WebDriver APIs. It acts as an abstraction layer to make it easier to write and run end to end tests against AngularJS web applications.

How it works

protractor-processes

Control Flow

One very important aspect in Protractor/webdriverjs is that it put your test code into the ‘controlFlow’ which is a promise based flow implicitly. 

With promises, the sequence code would be:

// pseudo code
driver.get(page1).then(function () {
	driver.click(E1);
});

Do you smell callback hell in there? To make it more neat, WebDriverJS has a wrapper for Promise called as ControlFlow.

In simple words, this is how ControlFlow prevents callback hell:

  • It maintains a list of schedule actions.
  • The exposed functions in WebDriverJS do not actually do their stuff, instead they just push the required action into the above mentioned list.
  • ControlFlow puts every new entry in the then callback of the last entry of the list, thus ensuring the sequence between them.

And so, it enables us to simply do:

// pseudo code
driver.get(page1);
// Implicitly add to previous action's then()
driver.click(E1);

Isn’t that awesome!

More about controlFlow and Frame

一个中文文章

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s