Unit testing Angular from Node like a boss

Building Angular application from CommonJS modules and powerful unit testing.

I previously wrote how to quickly unit test an Angular application using ng-describe. While this library is useful (we use it every day at Kensho), we can do better. In this tutorial I will show how to:

  • Use CommonJS modules in your application
  • Run Angular unit tests under Node without any browsers
  • Test private functions and Angular providers; something that is impossible to do using normal means.

The code

The working code for this tutorial is inside bahmutov/test-ng-from-node-with-code-extraction-tutorial. You can clone the repo to local disk, install the dependencies, run the tiny Angular application and most importantly run the unit tests

git clone [email protected]:bahmutov/test-ng-from-node-with-code-extraction-tutorial.git
cd test-ng-from-node-with-code-extraction-tutorial
npm install
open index.html
npm test

I tagged different points in the repository to allow you to see how the code developed and be able to play with it.

The initial application

Let us make a simple application that adds numbers. First, we need AngularJS library

npm install --save angular

Second, we need a page

touch index.html

We can place all the code into the page for now

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
<title>Angular App</title>
<script src="node_modules/angular/angular.js"></script>
<script>
angular.module('Application', [])
.controller('ApplicationController', function ($scope) {
$scope.add = function (a, b) {
return a + b;
};
});
</script>
</head>
<body ng-app="Application" ng-controller="ApplicationController">
<h2>Angular App</h2>
<p>2 + 3 = {{ add(2, 3) }}</p>
</body>
</html>

If you open the index.html in the browser you will see the result

Angular App
2 + 3 = 5

You can see this result for yourself under the tag step-0.

The application is working, now let us refactor: the application JavaScript code should live separately from the page, etc.

Separate JavaScript code

Let us first move all JavaScript code into separate files in the src folder.

md src
cd src
touch calc.js application.js

We can place the addition code into the calc.js, using value provider for simplicity

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

Our application will grab the add function from Calc module and will attach it to the scope so it could be used from the index.html template.

application.js
1
2
3
4
angular.module('Application', ['Calc'])
.controller('ApplicationController', function ($scope, add) {
$scope.add = add;
});

Finally, the page itself must include both calc.js and application.js

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>Angular App</title>
<script src="node_modules/angular/angular.js"></script>
<script src="src/calc.js"></script>
<script src="src/application.js"></script>
</head>
<body ng-app="Application" ng-controller="ApplicationController">
<h2>Angular App</h2>
<p>2 + 3 = {{ add(2, 3) }}</p>
</body>
</html>

This code is at tag step-1.

Start unit testing

Let us start unit testing code. We need the browser! And Karma! And configuration! But we want to unit test like a boss, which in my mind means being able to unit test individual components quickly. If we used CommonJS modules, we could do it very quickly

npm install --save-dev mocha
cd src
touch add.js add-spec.js

We could move function add to add.js and write BDD tests in add-spec.js

add.js
1
2
3
4
function add(a, b) {
return a + b;
}
module.exports = add;
add-spec.js
1
2
3
4
5
6
7
8
9
describe('add', function () {
var add = require('./add');
it('adds numbers', function () {
console.assert(add(2, 3) === 5);
});
it('concatenates strings', function () {
console.assert(add('foo', 'bar') === 'foobar');
});
});

If we have mocha installed globall (npm install -g mocha) we could run a single test suite using mocha src/add-spec.js, or we could run mocha via NPM package script

package.json
1
2
3
"scripts": {
"test": "mocha src/*-spec.js"
}

We can easily run unit tests with matching string using --grep or -g for short option

mocha -g "concatenates" src/add-spec.js
// runs just a single unit test 'concatenates strings'

Ok, we can now either run an individual spec file or all unit tests using npm test command.

But we copied the add function from calc.js and pasted the code into add.js. Now we need to load the add.js and use it from our Angular code. Our unit test is using CommonJS require, but the angular code executes in the browser where the require is unavailable. We need to build the application code from CommonJS code.

Building application code with webpack

To convert all code necessary from CommonJS and bundle it up to be run in the browser we are going to use webpack

npm install --save-dev webpack

Add build command to the package.json scripts

package.json
1
2
3
4
"scripts": {
"build": "webpack src/add.js src/calc.js src/application.js built.js",
"test": "mocha src/*-spec.js"
}

Modify calc.js to load the addition funciton exported from add.js file

calc.js
1
2
angular.module('Calc', [])
.value('add', require('./add'));

Run the build

npm run build

Which will produce a single file built.js which replaces the separate script tags in our index.html

index.html
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>Angular App</title>
<script src="node_modules/angular/angular.js"></script>
<script src="built.js"></script>
</head>
<body ng-app="Application" ng-controller="ApplicationController">
<h2>Angular App</h2>
<p>2 + 3 = {{ add(2, 3) }}</p>
</body>
</html>

You can find this code at the tag step-3

Unit testing Angular code

So far we could unit test pieces of code that do not touch Angular library. Now let us create a synthetic browser environment and load the Angular library directly from Node. I am using benv module for emulating window and document under Node.

npm install --save-dev benv
touch src/calc-spec.js

Let us place unit testing code into src/calc-spec.js. First, we will create the synthetic browser environment before each unit test. We will wipe it clean after each unit test

calc-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var benv = require('benv');
describe('calc module', function () {
beforeEach(function setupEnvironment(done) {
benv.setup(function () {
benv.expose({
angular: benv.require('../node_modules/angular/angular.js', 'angular')
});
done();
});
});
// more stuff will go here
afterEach(function destroySyntheticBrowser() {
benv.teardown(true);
});
});

After we setup the synthetic browser environment, we need to load the angular module Calc from file calc.js. We need to make sure to load it from scratch to prevent caching artifacts.

calc-spec.js
1
2
3
4
5
6
7
8
9
10
11
var benv = require('benv');
describe('calc module', function () {
// beforeEach setupEnvironment code
beforeEach(function loadCalcModule() {
// force to load the module from scratch
delete require.cache[require.resolve('./calc')];
require('./calc');
});
// unit test goes here
// afterEach destroySyntheticBrowser code
});

Now we are ready to unit test the calc.js - which has single module 'Calc' that provides single value under name 'add'

calc.js
1
2
angular.module('Calc', [])
.value('add', require('./add'));

We load calc.js directly from Node environment, thus the require call works right away, without any preprocessing. Here is the unit test we could use

calc-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
var benv = require('benv');
describe('calc module', function () {
// beforeEach setupEnvironment code
// beforeEach loadCalcModule code

it('has "add" value that adds numbers', function () {
var injector = angular.injector(['Calc']);
var add = injector.get('add');
console.assert(add(2, 3) === 5, 'added 2 + 3');
});

// afterEach destroySyntheticBrowser code
});

We are grabbing the injector for module 'Calc' (already created by the Angular system when loading file calc.js). Then we grab the addition function using name add. Then we verify that the addition function sums numbers correctly.

We can verify this runs from the command line

$ npm test
> [email protected] test /git/tutorial
> mocha src/*-spec.js
  add
    ✓ adds numbers
    ✓ concatenates strings
  calc module
    ✓ has "add" value that adds numbers
  3 passing (102ms)

Nice!

Test what you cannot touch

We often have code in our files that is NOT exported, and thus can be tested only indirectly. For a very artificial example, let us say that instead of using provider value, we provide an add service in our application.

calc.js
1
2
3
4
angular.module('Calc', [])
.service('add', function addService() {
return require('./add');
});

The unit tests still work the same since we use the dependency injection and we get the add function returned by addService. Imagine there is some additional function inside the file, but it is neither called nor exported.

calc.js
1
2
3
4
5
6
7
function hello() {
return 'hello';
}
angular.module('Calc', [])
.service('add', function addService() {
return require('./add');
});

How can we unit test the function hello? This is where the code extraction technique comes handy. Basically, we can use our own version of Node's require called really-need which allows preprocessing the loaded source code before compiling it. The project describe-it for example loads a given module inside a describe block and extracts any function / variable so you can unit test it, even if it is not exported.

npm install --save-dev describe-it

We get a function describeIt that creates its own set of tests, and can give you the desired function based on signature. It is additional code next to the existing unit tests

calc-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var benv = require('benv');
describe('calc module', function () {
// beforeEach setupEnvironment code
// beforeEach loadCalcModule code
// it 'has "add" value that adds numbers' test
var describeIt = require('describe-it');
var setupEachTime = true;
describeIt(__dirname + '/calc.js', 'hello()', setupEachTime, function (getHello) {
it('has private "hello"', function () {
var hello = getHello();
console.assert(hello() === 'hello');
});
});
// afterEach destroySyntheticBrowser code
});

The line describeIt(__dirname + '/calc.js', 'hello()', true, function (getHello) loads the file calc.js, looks for function signature hello(), and then will return the found function reference whenever you call getHello().

Pretty cool, see the code at step-5 tag.

Test the provider function

Let us imagine that the angular module Calc has the second function sub that is implemented using add like this

calc.js
1
2
3
4
5
6
7
8
9
angular.module('Calc', [])
.service('add', function addService() {
return require('./add');
})
.service('sub', function subService(add) {
return function sub(a, b) {
return add(a, -b);
};
});

We can easily unit test the returned sub function, but can we unit test the function subService itself? Not through the dependency injection - it returns the result function sub. But we can grab the function subService using the code extraction trick. For simplicity, move the function subService(add) to be function declaration

calc.js
1
2
3
4
5
6
7
8
9
10
function subService(add) {
return function sub(a, b) {
return add(a, -b);
};
}
angular.module('Calc', [])
.service('add', function addService() {
return require('./add');
})
.service('sub', subService);

Then write unit test using describeIt that grabs the function with the subService(add) signature

calc-spec.js
1
2
3
4
5
6
describeIt(__dirname + '/calc.js', 'subService(add)', setupEachTime, function (getSubService) {
it('has function', function () {
var fn = getSubService();
console.assert(typeof fn === 'function');
});
});

The unit test passes - we do grab the function reference. We can even execute the function to get the actual sub function, except it has a problem

calc-spec.js
1
2
3
4
5
6
7
8
9
10
11
describeIt(__dirname + '/calc.js', 'subService(add)', setupEachTime, function (getSubService) {
it('has function', function () {
var fn = getSubService();
console.assert(typeof fn === 'function');
});
it('returns sub', function () {
var fn = getSubService();
var sub = fn();
console.assert(sub(2, 3) === -1);
});
});
calc module subService(add) returns sub:
    TypeError: undefined is not a function

The problem is that it tries to call add argument from sub - which we never provided! When the code run, the angular dependency injection finds add function and injects it into the returned function. When we just did it ourselves, we did not provide anything. Fortunately it is simple to do

1
2
3
4
5
6
7
8
it('returns sub', function () {
var fn = getSubService();
function testAdd(a, b) {
return a + b;
}
var sub = fn(testAdd);
console.assert(sub(2, 3) === -1);
});

Runs perfectly. We can even spy on testAdd to make sure it was called.

1
2
3
4
5
6
7
8
9
10
11
it('calls the provided testAdd', function () {
var fn = getSubService();
var testAddCalled;
function testAdd(a, b) {
testAddCalled = true;
return a + b;
}
var sub = fn(testAdd);
console.assert(sub(2, 3) === -1);
console.assert(testAddCalled);
});

Great, find the code at tag step-6.

Let Angular Dependency Injection do all the work

In the previous example, we had to create our own function testAdd to inject into the extracted function subService(add). But there is already the function add provided by the module "Calc". We can grab it via Angular's own dependency injection

calc-spec.js
1
2
3
4
5
6
it('works with Angular own injection', function () {
var subService = getSubService();
var injector = angular.injector(['Calc']);
var sub = injector.invoke(subService);
console.assert(sub(2, 3) === -1);
});

When we use the angular.injector(['Calc']).invoke(subService), the dependency injection will use the argument names (extracted by inspecting the subService.toString() output) to find what to inject. We can even "fool" the dependency injection by listing different names to be injected. For example, let us provide "hello" service in the "Calc" module

calc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hello() {
return 'hello';
}
function subService(add) {
return function sub(a, b) {
return add(a, -b);
};
}
angular.module('Calc', [])
.service('add', function addService() {
return require('./add');
})
.value('hello', hello)
.service('sub', subService);

in our unit test, when extracting subService we can tell the dependency injection that the subService needs hello as the first argument (instead of "add").

calc-spec.js
1
2
3
4
5
6
7
it('can inject different stuff', function () {
var subService = getSubService();
var injector = angular.injector(['Calc']);
subService.$inject = ['hello'];
var sub = injector.invoke(subService);
console.assert(sub(2, 3) === 'hello');
});

We grabbed the subService using code extraction. Before we let the dependency injection do its job we list the names to be injected manually. Instead of letting the DI inspect the function signature subService(add) and finding "add" argument name, we tell the DI that the function really needs to inject "hello" service. Thus the sub(2, 3) is really executing the code below

1
2
3
4
5
function subService(hello /* injected instead of add */) {
return function sub(a, b) {
return hello(a, b);
};
}

which always produces "hello"!

You can see this code at step-7 tag.

Combined custom and Angular dependency injections

Can we combine the two types of injectors (our own and Angular) in the same code? Yes, using partial argument binding library like heroin. This little helper works by binding only some arguments by name. For example, if we have a function with 3 arguments

1
2
3
4
5
6
7
8
9
10
11
// npm install --save-dev heroin
function add(a, b, c) { return a + b + c; }
var heroin = require('heroin');
var values = {
a: 10,
c: 100
};
var adder = heroin(add, values);
// adder = function(a = 10, b, c = 100) ...
adder(4); // 4 goes to the only "free" argument "b"
// returns 114

Let us imagine that the 'Calc' module provides a service to compute the sum asynchronously.

calc.js
1
2
3
4
5
6
7
8
9
10
function addAsyncService($q, add) {
return function addAsync(a, b) {
return $q.when(add(a, b));
};
}
angular.module('Calc', ['ng'])
.service('add', function addService() {
return require('./add');
})
.service('addAsync', addAsyncService);

The function addAsync uses service $q from the standard module ng to return a promise. The promise will be resolved with the result of the add service (also injected from 'Calc' module), and called synchronously. Ok, let us do the dependency injection in 2 steps, but first writing a new suite. We will extract the function addAsyncService($q, add) and will load the heroin function.

calc-spec.js
1
2
3
4
5
6
7
describeIt(__dirname + '/calc.js', 'addAsyncService($q, add)', setupEachTime, function (getFn) {
var heroin = require('heroin');
it('can inject some of our mocks and let the angular inject the rest', function (done) {
var addAsyncService = getFn();
// the rest of the unit test goes here
});
});

To better test the asynchronous add, let us use our own little utility function that will verify the passed in argument and would return some unusual result. This way we will easily see if our mock add was called and not the real thing.

1
2
3
4
5
6
7
8
9
10
it('can inject some of our mocks and let the angular inject the rest', function (done) {
var addAsyncService = getFn();
function mockAdd(a, b) {
console.assert(a === 2 && b === 3);
return 42;
}
var mockedAdd = heroin(addAsyncService, {
add: mockAdd
});
});

We just created a new function mockedAdd from the function addAsyncService. If we look at their effective signatures they are

function addAsyncService($q, add)
function mockedAdd($q /* add = mockAdd */)
// mockedAdd will call addAsyncService

What about the $q service? We could have injected our own promise-returning object with method $q.when, but we could just let the Angular inject the true $q. We just need to tell it the name of the remaining argument in mockedAdd - the heroin messes up the argument names, since it is a dynamic function that uses arguments object. The complete test is

calc-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
it('can inject some of our mocks and let the angular inject the rest', function (done) {
var addAsyncService = getFn();
function mockAdd(a, b) {
console.assert(a === 2 && b === 3);
return 42;
}
var mockedAdd = heroin(addAsyncService, {
add: mockAdd
});
mockedAdd.$inject = ['$q'];
var injector = angular.injector(['Calc']);
var addAsync = injector.invoke(mockedAdd);
addAsync(2, 3).then(function (sum) {
console.assert(sum === 42);
done();
});
});

Crazy, but the two injectors work quite nicely together, and you can see it for yourself under tag step-8.

Conclusions

This tutorial showed how to write Angular application like a boss: instead of concatenating files, obsessing over loading order, loading the entire application, the boss just requires stuff. Even better, using CommonJS modules makes unit testing from Node simpler. You want to run just a single spec? No problem, no editing karma.conf.js necessary. Just a single command needed:

mocha src/foo-spec.js

What if your code is a collection of little functions, preferably pure, returning the result that only depends on the inputs? How can you test them, if they are just sitting there, hidden from the outside world behind the module.exports interface? Like a boss, you just grab them by name!

1
describeIt('filename.js', 'add(a, b)', ...);

This code extraction feature does not require any source code modifications and works like magic using a source code preload hook inside the Node's require. No extra work on your part.

Finally, instead of relying only on the Angular Dependency Injection, you can extract the entire provider code, substitute your own mocks or even use your own injector.

1
2
3
4
var subService = getSubService();
var injector = angular.injector(['Calc']);
var sub = injector.invoke(subService);
console.assert(sub(2, 3) === -1);

You get the provider code much sooner, before the Angular dependency injection has a change to limit what you can to the returned value. You are the boss; only the full service for you!

As a very last observation, note that we did not load or use angular-mocks library in our unit tests. No dummy modules, weird delayed digest cycles or artifical back end responses. Pure production-grade Angular running in your unit tests, bridging the gap between your application and test code.

3rd party tools in this tutorial

  • mocha - excellent BDD unit testing framework that works great under Node.
  • benv - simulates browser and document environment which allows front end code to run under Node.
  • really-need - a replacement for Node's built-in require with some nice features.
  • describe-it - code extraction tool for unit testing. Grabs private functions or variables, making them available for unit testing.
  • heroin - an addictive partial argument application by name.