1, 2, 3, tested

Unit testing AngularJS code in record time using ng-describe.

Testing AngularJS requires too much boilerplate code. We were so fed up with writing extra lines, looking up the syntax, etc. that we wrote a helper library kensho/ng-describe with a single function. Yet that function can hide all the gory AngularJS details, allowing you to write unit tests in record times. In this tutorial I will show how to start unit testing an AngularJS code, how to get code coverage, and how to test different Angular features using ng-describe library.

The code to go with this tutorial is available at bahmutov/unit-testing-angularjs-tutorial. I advise to clone that repo to a local folder, checkout each tag as you read the tutorial, try running the code and the unit tests.

git clone [email protected].com:bahmutov/unit-testing-angularjs-tutorial.git
cd unit-testing-angularjs-tutorial
npm install

Unit testing JavaScript code

step-0

You can find the source for the following section under tag step-0. If you checked out the repository, run git checkout step-0 to get to this commit.

Let us start with the simplest example to get a feel for Jasmine framework and Karma test runner. First we need to write the simplest function one can test.

add.js
1
2
3
function add(a, b) {
return a + b;
}

There are no unit tests yet - we are not sure how the add function behaves when we pass numbers or strings or undefined values. We need to unit test the code in add.js.

Let us create a spec file - in the Behavior Driven Development (BDD for short) testing language we are going to use, the spec file specifies how the add function behaves. Typically, I give the spec file the same name as the source file plus -spec.js suffix.

add-spec.js
1
2
3
4
5
describe('add', function () {
it('adds numbers', function () {
expect(add(2, 3)).toEqual(5);
});
});

File add-spec.js describes the add function, telling us that it (meaning add) adds numbers. When we add number 2 to number 3 we expect to get back number 5.

Where do functions describe and it come from? From the Jasmine testing library we are going to use to execute the unit tests. While we could get a stand alone Jasmine command line tool, a much better solution is to use Karma test runner. It has Jasmine adapter for executing Jasmine unit tests, plus can do a lot more. It is easier to just start using it right away.

step-1

In the project folder, I added Karma configuration file. You can create one if you install Karma globally (npm install -g karma) and then call karma init. Here is the file I generated

karma.conf.js
1
2
3
4
5
6
7
8
9
10
11
module.exports = function(config) {
config.set({
frameworks: ['jasmine'],
files: [
'*.js'
],
port: 9876,
browsers: ['Chrome'],
singleRun: true
});
};

Then I installed Karma and the necessary plugins (like Jasmine, Chrome launcher) in the project's folder.

npm install --save-dev karma karma-jasmine karma-chrome-launcher

Then I added the karma start command as the package test script. Here is the package.json

package.json
1
2
3
4
5
6
7
8
9
"scripts": {
"test": "karma start"
},
"devDependencies": {
"jasmine-core": "2.3.4",
"karma": "0.13.9",
"karma-chrome-launcher": "0.2.0",
"karma-jasmine": "0.3.6"
}

You can now execute npm test command, which will flash Chrome browser and will report a single successfully passed unit test.

$ npm test
> [email protected]1.0.0 test /Users/gleb/git/unit-testing-angularjs-tutorial
> karma start
15 08 2015 17:28:06.164:INFO [karma]: Karma v0.13.9 server started at http://localhost:9876/
15 08 2015 17:28:06.169:INFO [launcher]: Starting browser Chrome
15 08 2015 17:28:07.260:INFO [Chrome 44.0.2403 (Mac OS X 10.10.2)]: 
    Connected on socket aU7IWF2FR43DDxjmAAAA with id 6382992
Chrome 44.0.2403 (Mac OS X 10.10.2): Executed 1 of 1 SUCCESS (0.006 secs / 0.002 secs)

You can see the code and run the test command by using tag step-1

step-2

Before we run each unit test, we might need to set things up. In the simple source example above, let us verify that the add is an existing function. In Jasmine (and other BDD testing libraries) one can perform actions before each unit test by registering a single or multiple callbacks

add-spec.js
1
2
3
4
5
6
7
8
describe('add', function () {
beforeEach(function () {
expect(typeof add).toEqual('function');
});
it('adds numbers', function () {
expect(add(2, 3)).toEqual(5);
});
});

Running npm test should still pass, but if we expected add to be an object instead of a function we would get an error

add-spec.js
1
2
3
4
5
6
describe('add', function () {
beforeEach(function () {
expect(typeof add).toEqual('object');
});
...
});
$ npm test
> [email protected]1.0.0 test /Users/gleb/git/unit-testing-angularjs-tutorial
> karma start
15 08 2015 17:32:28.636:INFO [karma]: Karma v0.13.9 server started at http://localhost:9876/
15 08 2015 17:32:28.641:INFO [launcher]: Starting browser Chrome
15 08 2015 17:32:30.081:INFO [Chrome 44.0.2403 (Mac OS X 10.10.2)]: 
    Connected on socket tjX2X1rAcWJHTSmJAAAA with id 38618292
Chrome 44.0.2403 (Mac OS X 10.10.2) add adds numbers FAILED
    Expected 'function' to equal 'object'.
        at Object.<anonymous> (/Users/gleb/git/unit-testing-angularjs-tutorial/add-spec.js:3:24)
Chrome 44.0.2403 (Mac OS X 10.10.2): Executed 1 of 1 (1 FAILED) ERROR (0.009 secs / 0.004 secs)
npm ERR! Test failed.  See above for more details.

We will use beforeEach callbacks to initialize AngularJS state before running unit tests in the future steps. Note, there is also afterEach callback, and you can register multiple before and after functions to run.

step-3

It is important for the unit tests to execute most if not every line of the source code. One can check if the unit tests do this by computing code coverage automatically every time the unit tests run. This is very simple to do using another Karma plugin.

npm install --save-dev karma-coverage

Add coverage preprocessor for the JavaScript files (typically I prefer to only instrument the source files, not the specs), and add the coverage reporter to save the results

karma.conf.js
1
2
3
4
5
6
7
8
9
module.exports = function(config) {
config.set({
...
preprocessors: {
'*.js': 'coverage'
},
reporters: ['progres', coverage']
});
};

Run the command npm test and then open the coverage report page in the browser

open coverage/Chrome<...>/index.html

It will show code coverage information line by line for both add.js and add-spec.js. We can see that we have hit every line of the add.js source file

code coverage

If we add some other code to the add.js, we will see missed lines after executing the same tests

add.js
1
2
3
4
5
6
function add(a, b) {
return a + b;
}
if (false) {
add = undefined;
}

The coverage shows missed line and missed branch if (false).

missed line

Code coverage will become invaluable when you want to target the unit tests to the specific areas of the code before refactoring or fixing a bug.

step-4

Let us make a tiny Angular application that will add numbers. The simplest way to use our addition function is to make it a value of an Angular module.

1
2
3
4
angular.module('Calc', [])
.value('add', function add(a, b) {
return a + b;
});

We now need AngularJS library and if we want to test it, angular-mocks helper library.

npm install --save angular
npm install --save-dev angular-mocks

We should add these two libraries to the Karma configuration file, loading them before our source files.

karma.conf.js
1
2
3
4
5
6
7
8
9
10
module.exports = function(config) {
config.set({
files: [
'node_modules/angular/angular.js',
'node_modules/angular-mocks/angular-mocks.js',
'*.js'
],
...
});
};

This tutorial is mainly about using ng-describe to perform AngularJS unit testing with minimum boilerplate code, but just for fun, let us first write a unit test using just the angular-mocks library. Modify the add-spec.js to load the Calc module and then inject add value.

add-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
describe('add', function () {
var add;
beforeEach(function () {
angular.mock.module('Calc');
angular.mock.inject(function (_add_) {
add = _add_;
expect(typeof add).toEqual('function');
})
});
it('adds numbers', function () {
expect(add(2, 3)).toEqual(5);
});
});

Notice the extra lines and calls to first load the module Calc to be available during the testing, second to inject the function provided by the value('add', function ...) We also had to surround normal name add with underscores in inject(function (_add_) { ... to avoid clashing with the local variable add.

This is a common theme when unit testing AngularJS code - things hidden by the framework's dependency injection and other "magic" code are suddenly exposed and require non-trivial amount of code to be setup during unit testing. To get even better perspective, let us write a controller that would use add function to perform additions.

step-5

I created simple page that adds user-entered two number. The markup:

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>Addition</title>
<script src="node_modules/angular/angular.js"></script>
<script src="add.js"></script>
<script src="add-app.js"></script>
</head>
<body ng-app="AddApp" ng-controller="AddController">
<input type="number" ng-model="a" /> + <input type="number" ng-model="b" /> =
<input type="number" ng-model="sum" />
</body>
</html>

The code for AddApp module will be in add-app.js

add-app.js
1
2
3
4
5
6
7
8
9
10
angular.module('AddApp', ['Calc'])
.controller('AddController', function ($scope, add) {
$scope.a = $scope.b = $scope.sum = 0;
$scope.$watch('a', function () {
$scope.sum = add($scope.a, $scope.b);
});
$scope.$watch('b', function () {
$scope.sum = add($scope.a, $scope.b);
});
});

The result is simple to see, you can open the index.html file using any browser locally

open index.html

Enter two numbers and see their sum

AddApp

We have already unit tested the add function using angular-mocks and have encountered writing a little bit of boilerplate code. Let us unit test the controller AddController if it is an easy thing to do using angular-mocks.

step-6

Let us test if the controller AddController initializes properties a, b and sum to zeroes. It must be simple, right?

add-controller-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe('add controller', function () {
var $controller, $rootScope, $scope;
beforeEach(function () {
angular.mock.module('AddApp');
});
beforeEach(angular.mock.inject(function (_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
}));
beforeEach(function () {
$scope = $rootScope.$new();
$controller('AddController', { $scope: $scope });
});
it('sets a, b and sum to zero', function () {
expect($scope.a).toEqual(0);
expect($scope.b).toEqual(0);
expect($scope.sum).toEqual(0);
});
});

The unit test looks nothing like the simplet AngularJS application code. The same magic that works so nicely behind the scenes now requires the developer to work to execute. We had to use 3 beforeEach callbacks to set things up before our unit test

  • Load the module under test AddApp, which is reasonable
  • Inject both $controller and $rootScope services - something we almost never do in the normal AngularJS code
  • Create a new scope object using $rootScope.$new method and then create the controller instance.

This is a lot of boilerplate code just to get the ball rolling.

step-7

Let us use the kensho/ng-describe to remove all the boilerplate code from the above unit test. First install the library and add it to the karma.conf.js before your specs

npm install --save-dev ng-describe
karma.conf.js
1
2
3
4
5
6
files: [
'node_modules/angular/angular.js',
'node_modules/angular-mocks/angular-mocks.js',
'node_modules/ng-describe/dist/ng-describe.js',
'*.js'
],

ng-describe is a library with a minimal api. It is a single function that takes an object of options

ngDescribe({ ... });

That is it. You can think that ngDescribe replaces describe function calls used above to perform Angular-specific initialization before each unit test. For example, to test add function from the add-spec.js change the code to do the following

add-spec.js
1
2
3
4
5
6
7
8
9
ngDescribe({
module: 'Calc',
inject: 'add',
tests: function (deps) {
it('adds numbers', function () {
expect(deps.add(2, 3)).toEqual(5);
});
}
});

Unit test adds numbers needs function add provided by the module Calc. The module loading and add injection happen because we specified the module and the list of services to inject

1
2
3
4
5
ngDescribe({
module: 'Calc',
inject: 'add',
...
});

You can specify multiple modules and multiple values to inject. All injected values will be properties of the first argument to the special test callback. In my code I usually name the first argument deps, which is short for dependencies. Thus the unit test just calls deps.add inside:

1
2
3
4
5
6
7
8
ngDescribe({
...
tests: function (deps) {
it('adds numbers', function () {
expect(deps.add(2, 3)).toEqual(5);
});
}
});

This is how ng-describe works - it does a lot of Angular-specific work behind the scenes, placing the result into the deps argument. The unit tests go inside the test callback, and have full access to the deps object because it encloses them.

step-8

Let us look how much boilerplate ng-describe removes when testing the controller AddController. We already saw that unit testing a controller using angular-mocks requires a lot of code just a controller instance. Here is the same unit test switched to ng-describe

1
2
3
4
5
6
7
8
9
10
11
ngDescribe({
module: 'AddApp',
controller: 'AddController',
tests: function (deps) {
it('sets a, b and sum to zero', function () {
expect(deps.AddController.a).toEqual(0);
expect(deps.AddController.b).toEqual(0);
expect(deps.AddController.sum).toEqual(0);
});
}
});

That is it. We tell ng-describe that we need a controller AddController and it does the rest behind the scenes. What is the object deps.AddController then? In most cases when creating a controller, we really need the $scope object. Thus the ng-describe when creating a controller, places its created scope into the object passed to the tests. The unit tests then interact with the scope object

expect(deps.AddController.a).toEqual(0);
// logically same as
expect($scope.a).toEqual(0);

step-9

We have only tested the initial values set inside the controller. Can we test the summation logic too? If we set the values of a and b in the controller's scope, we should get the property sum updated too. Here is the second unit test we can add to the add-controller-spec.js

1
2
3
4
5
6
7
8
9
10
11
12
13
ngDescribe({
module: 'AddApp',
controller: 'AddController',
tests: function (deps) {
// first unit test as before
it('computes the sum', function () {
deps.AddController.a = 10;
deps.AddController.b = 20;
deps.step();
expect(deps.AddController.sum).toEqual(30);
});
}
});

The deps.step() method is equivalent to $rootScope.$digest() or deps.AddController.$apply(); but removes need to inject the $rootScope dependency or remember the syntax. It is up to you which one you prefer.

step-10

We will finish this tutorial with another powerful ng-describe feature: simple server response mocking. Sometimes our application has to make requests to the server. For example, imagine that instead of computing the sum on the client side, we send both numbers a and b to a server that performs the computation and returns the sum.

add.js
1
2
3
4
5
6
7
8
9
10
11
angular.module('RemoteCalc', [])
.service('add', function ($http) {
return function add(a, b) {
return $http.get('/add/', {
params: {
a: a,
b: b
}
});
};
});

We will change our application code to use RemoteCalc instead of plain Calc to provide add, and will use the resolved value instead of the returned result

add-app.js
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('AddApp', ['RemoteCalc'])
.controller('AddController', function ($scope, add) {
$scope.a = $scope.b = $scope.sum = 0;
function computeSum() {
add($scope.a, $scope.b)
.success(function (response) {
$scope.sum = response;
});
}
$scope.$watch('a', computeSum);
$scope.$watch('b', computeSum);
});

If we run the unit tests now, we get an error - an Ajax request!

Chrome 44.0.2403 (Mac OS X 10.10.2) default tests computes the sum FAILED
    Error: Unexpected request: GET /add/?a=10&b=20

Of course, our unit test sets values 10 and 20, the controller makes the request right away!

add-controller-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
ngDescribe({
module: 'AddApp',
controller: 'AddController',
tests: function (deps) {
it('computes the sum', function () {
deps.AddController.a = 10;
deps.AddController.b = 20;
deps.step();
expect(deps.AddController.sum).toEqual(30);
});
}
});

We need to return 30 in this case, luckily it is simple to do using ngDescribe. Just add another property to the ngDescribe options with values to be returned for each expected HTTP request

add-controller-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ngDescribe({
module: 'AddApp',
controller: 'AddController',
http: {
get: {
'/add/?a=10&b=20': 30
}
},
tests: function (deps) {
it('computes the sum', function () {
deps.AddController.a = 10;
deps.AddController.b = 20;
deps.step();
expect(deps.AddController.sum).toEqual(30);
});
}
});

When during the course of the unit test, the controller uses RemoteCalc.add service, which calls $http.get and executes the GET /add/?a=10&b=20 - the mock backend will be ready to return 30.

step-11

While mocking the responses to the expected http requests is powerful, I would argue that the unit test gets way too deep to the implementation details. We are testing AddController, yet we looked inside the RemoteCalc to find that it makes a GET request to the server. The unit test is mocking an action very far from the code under the test itself. Can we do better?

Yes we can using mocks, which are pieces of fake code to be used instead of the real code during unit tests. For example, the entire actual RemoteCalc.add service making the GET http request can be replaced with a mock for the duration of the unit test. With ng-describe it is simple

add-controller-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ngDescribe({
module: 'AddApp',
controller: 'AddController',
mock: {
AddApp: {
add: function ($q, a, b) {
return $q.when(a + b);
}
}
},
tests: function (deps) {
it('computes the sum', function () {
deps.AddController.a = 10;
deps.AddController.b = 20;
deps.step();
expect(deps.AddController.sum).toEqual(30);
});
}
});

Using the special property mock we are setting fake implementation to be injected by the Angular dependency injection:

1
2
3
4
5
6
7
mock: {
AddApp: {
add: function ($q, a, b) {
return $q.when(a + b);
}
}
}

Whenever module AddApp injects service add, the environment will provide our fake function

1
2
3
function ($q, a, b) {
return $q.when(a + b);
}

Notice that ng-describe is smart enough to inject other services into the fake code. For example, because $http.get returns a promise, we need to return a promise too from the fake code. We do this by asking for the $q service and returning $q.when(expression). This is every better code than mocking http because it will return a correct computed sum a+b instead of hardcoded 30 for a single expected request /add/?a=10&b=20.

Conclusion

I hope this tutorial set you on the path to effectively unit test your AngularJS application code. You can now target a particular code with the tests and write a test with minimum boilerplate.

The ng-describe/README.md has lots of examples showing every supported test feature, including testing directives, spying on services and other boilerplate-prone test cases. I would start by reading the examples. If there is a question, or a particular testing feature missing, please open a Github issue with a detailed example; including the code to be tested and the unit test that is not working.

Additional information