title | slug | createdAt | language | preview |
---|---|---|---|---|
Unit testing Angular.js app with node.js, mocha, angular-mocks and jsdom |
unit-testing-angular-js-app-with-node |
2015-07-05T18:04:33Z |
en |
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. |
Lean unit tests with minimal setup
- 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)
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.
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;
If you have installed Angular with Bower:
bower install angular-mocks -D
If you have installed Angular with npm:
npm install angular-mocks -D
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;
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.
If you want to also mock the modules your services (or what ever you are testing) are using I would recommend proxyquire.
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;
Ah, I figured it out. It's a combination of several things. First of all, I am running a script to programatically add my tests then run them. At the point I was including angular-mocks,
window.mocha
andbeforeEach
wasn't in scope, so it wasn't loading. I ended up with this:then, my tests are super-elegant, and chai is all preconfigured with as-promised & sinon. here is an example test: