Getting Started
I've been a big fan of Jasmine for a while. I've blogged about it, presented it as a lunch and learn, and I teach it as part of our developer training here at Geneca. We use the Chutzpah Visual Studio extension to automatically run all Jasmine tests on builds and it's pretty sweet.
When I learned about
Jest I was really interested because when I'm writing apps in my spare time, I'm not using Visual Studio. More often than not I'm using Node. The more popular alternative to Jest is Karma. I know very little about Karma but I've seen it when Yeoman was used to generate an Angular app. It seems like it starts up an actual browser instance when it executes test, so it's great for end to end testing. Jest looks like it doesn't rely on a browser to execute your tests.
My stack
Ionic is a new (at current time) mobile framework that integrates well with Angular, and that sounds exciting to me. I've built a custom enterprise app using Kendo Mobile (now Telerik AppBuilder) and instead of using the built-in MVVM objects for data-binding I used Angular as I was more familiar and was having a difficult time with MVVM.
All in all I'm using Ionic, Angular, AngularUI Router, Angular-Animate, Angular-Sanitize, Angular-Mocks (just for testing), BindOnce, Lodash, and Gulp.
Getting all the right references
Ionic.bundle.js is a combination of several js files (as specified in the comments block at the top), and the version of Angular that is included by default was not compatible with Angular-Mocks. The version of Angular-Mocks that I used is linked at the end of this blog.
To mitigate this I use Jest to require all of the combined files separately, pulling in the correct version of Angular that I need. I actually have a gulp task to build me a new Ionic file that I rename to ionic.dynamic.bundle.js for the actual app.
I created a js file in the project root folder named testIncludes.js. Here are the contents:
var files = {
ionic: './www/lib/ionic/js/ionic',
angular: './www/lib/ionic/js/angular/angular',
angularAnimate: './www/lib/ionic/js/angular/angular-animate',
angularSanitize: './www/lib/ionic/js/angular/angular-sanitize',
angularUIRouter: './www/lib/ionic/js/angular-ui/angular-ui-router',
ionicAngular: './www/lib/ionic/js/ionic-angular',
angularMocks: './www/lib/ionic/js/angular/angular-mocks',
bindOnce: './www/lib/bindonce',
lodash: './www/lib/lodash.min',
app: './www/js/app',
areaService: './www/js/services/areaService',
store: './www/js/services/store'
};
window.Event = {};
var vals = {};
localStorage = {
getItem: function(key) { return vals[key]; },
setItem: function(key, value) { vals[key] = value + ''; },
clear: function() { vals = {}; }
};
jest
.dontMock(files.ionic)
.dontMock(files.angular)
.dontMock(files.angularAnimate)
.dontMock(files.angularSanitize)
.dontMock(files.angularUIRouter)
.dontMock(files.ionicAngular)
.dontMock(files.angularMocks)
.dontMock(files.lodash)
.dontMock(files.bindOnce)
.dontMock(files.app)
.dontMock(files.areaService);
_ = require(files.lodash);
require(files.ionic);
require(files.angular);
require(files.angularAnimate);
require(files.angularSanitize);
require(files.angularUIRouter);
require(files.ionicAngular);
require(files.angularMocks);
require(files.bindOnce);
require(files.app);
require(files.store);
require(files.areaService);
Some gotchas
There is a line that reads window.Event = {};
This line is needed as the Jest tests don't have access to some of the built-in window objects.
I also needed to wire up localStorage for the same reason.
I wish there was a way to combine telling Jest to dontMock a library in the same line that I require it. Seems kind of redundant.
Testing an angular service that relies on all the above plus another angular service
My angular service looks like this:
angular.module('locationApp').factory('AreaService',
function(Store) {
var areas = Store.areas;
function get(areaId) {
for (var idx in areas) {
if (areas[idx].id == areaId) return areas[idx];
}
}
return {
all: function() { return areas; },
get: get
};
}
);
The test file located in <root>/__tests__/services/areaService-test.js looks like this:
require("../testIncludes");
describe('area service', function() {
var storeMock,
service;
beforeEach(function () {
storeMock = { areas: [{id:1,name:'Area1'},{id:2,name:'Area2'}] };
angular.mock.module('locationApp', function($provide) {
$provide.value('Store', storeMock);
});
inject(function(_AreaService_) {
service = _AreaService_;
});
});
it('returns the correct number of areas from all() function', function() {
var areas = service.all();
expect(areas.length).toBe(2);
});
});
Before each test is run I mock out the Store object with some test data. The real Store service makes an actual ajax call for data, something I don't want to happen in my testing.
Most examples I've seen online for using Angular-Mocks say to just use the
module method in your beforeEach but I found that I had to use
angular.mock.module. Within that call you have the opportunity to override Angular's dependency injection provider to use your mock objects.
The
inject method is where you extract out the object(s) you want to run your tests against. I want to run tests against the AreaService object so I grab it from the inject method and assign it to a class-level variable that I can call from all my tests. Notice the leading and trailing underscores? Those are completely optional. The point is that if you use them, the mock library will strip them out when it resolves it to the right class, but it gives you the opportunity to give a local variable the same name if you so choose.
References:
At the time of this writing, I've been using Angular-Mock 1.2.16 obtained from this project:
https://github.com/angular/bower-angular-mocks/blob/master/angular-mocks.js
I also got some tips from this article as well:
http://www.benlesh.com/2013/06/angular-js-unit-testing-services.html
Here is part 2 on
Unit Testing an Angular Controller