Okay codes
Riku Rouvilahello@rikurouvila.fi +358504700715CVGitHub icon
Published on Jul 5th 2015

Testing Angular.js app headlessly with node.js + mocha

Lean unit tests with minimal setup

Code examples

Keypoints

  • Fake DOM (Everything works without a real browser)
  • Uses ngMocks to inject and mock Angular.js dependencies
  • I'm assuming you are already using browserify (but everything works fine without it)

Some background

Majority of search result about unit testing Angular.js apps is about how to do it by using test frameworks that run the tests in a real browser. Even though it's great to be able to test your code in multiple platforms, in my opinion it creates a lot of boilerplate code and makes it hard to run the tests in, for instance a CI-server.

In most cases I just want to include a module from my code to my tests, call methods from the module and do assertions to assure that everything is working properly.

1. Set up a mock DOM

npm install jsdom@~3.x.x -D

Even though jsdom 4.0 is already available by the time I'm writing this we will still use the 3.x version because the most recent versions support only io.js.

tests/test-helper.js

var jsdom = require('jsdom').jsdom;

global.document = jsdom('<html><head><script></script></head><body></body></html>');
global.window = global.document.parentWindow;
global.navigator = window.navigator = {};
global.Node = window.Node;

2. Install and set-up ngMocks

If you have installed Angular with Bower:

bower install angular-mocks -D

If you have installed Angular with npm:

npm install angular-mocks -D

:warning: Make sure your angular-mocks version matches your angular version

tests/test-helper.js

var jsdom = require('jsdom').jsdom;

global.document = jsdom('<html><head><script></script></head><body></body></html>');
global.window = global.document.parentWindow;
global.navigator = window.navigator = {};
global.Node = window.Node;

global.window.mocha = {};
global.window.beforeEach = beforeEach;
global.window.afterEach = afterEach;

/*
 * Only for Bower users
 */
require('../bower_components/angular');
require('../bower_components/angular-mocks');

/*
 * Only for NPM users
 */
require('angular/angular');
require('angular-mocks');

global.angular = window.angular;
global.inject = global.angular.mock.inject;
global.ngModule = global.angular.mock.module;

3. Install Mocha and write some tests

npm install mocha -D

I assume that your directory structure looks something similar to this

app/
  scripts/
    services/
      - userService.js
    controllers/
package.json

Lets create a new directory called __tests__ under our services/ directory and a new test file called userService.js there.

app/
  scripts/
    services/
      __tests__/
        - userService.js
      - userService.js
    controllers/
package.json

Here's a reference implementation of our userService.js

angular.module('myServices.user', []).service('UserService', function() {
  return {
    getUsers: function () {
      return [];
    }
});

app/scripts/services/__tests__/userService.js

var assert = require('assert');
require('../../../tests/test-helpers');

// Loads the module we want to test
require('../userService');

describe('User service', function() {
  beforeEach(ngModule('myServices.user'));
  
  it('should return a list of users', inject(function(UserService) {
    assert.equal(UserService.getUsers().length, 0);
  }));
});

Now by running ./node_modules/.bin/mocha app/scripts/**/__tests__/**/*.js we should see that our tests work.

Protip: You most likely want to add that to your package.json's scripts.test field.

"scripts": {
  "test": "mocha app/scripts/**/__tests__/**/*.js"
}

Notice how you don't have to use the ./node_modules... path anymore since npm resolves it for you. If you are using npm >=2.0 you can no also use npm test -- --watch to start Mocha in watch mode.

For Browserify users

If you want to also mock the modules your services (or what ever you are testing) are using I would recommend proxyquire.


Known issues

Broken --watch mode

If you've installed Angular.js with npm you'll notice that on Mocha's watch mode your fake DOM is recreated on every reload but the Angular.js code isn't re-evaluated which leads into thrown exceptions about angular being undefined. There's two causes for this: angular and angular-mocks are both singletons and Mocha doesn't re-evaluate anything in node_modules. This problem doesn't exist for Bower users since their angular and angular-mocks are in the bower_components directory.

Solution

Even though this is a bit dirty, I solved this by invalidating require cache for angular and related modules.

tests/test-helper.js

//...
delete require.cache[require.resolve('angular')];
delete require.cache[require.resolve('angular/angular')];
delete require.cache[require.resolve('angular-mocks')];

require('angular/angular');
require('angular-mocks');

global.angular = window.angular;
global.inject = global.angular.mock.inject;
global.ngModule = global.angular.mock.module;

I love feedback!

First of all: thank you so much for reading!
If you have something to ask, don't agree with something I say or just wanna have a chat, feel free to ping me on Twitter.

Freelance Software Developer in Tampere, FinlandRiku Rouvila
Software developer