Tuesday, June 3, 2014

Unit testing your Ionic + Angular app with Facebook's Jest

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


10 comments:

  1. Great post! Just curious what the issue was with mocks? Could be a version mismatch between the mocks version you tried and the version of Angular in the bundle?

    We use mocks for all our internal unit testing without problems so I want to figure out what's going on here and fix it.

    ReplyDelete
    Replies
    1. Yes indeed, the version of Angular included with Ionic was not compatible with Angular Mocks. That's why I had to include each library separately rather than the included Ionic bundle file for Jest testing. I made a gulp task that dynamically assembles all the files for me with the newer angular file when I'm testing the app itself.

      Delete

  2. Thank you so much for sharing this worth able content with us. The concept taken here will be useful for my future programs and i will surely implement them in my study. Keep blogging article like this.
    ionic training in chennai

    ReplyDelete
  3. Expected to form you a next to no word to thank you once more with respect to the decent recommendations you've contributed here. We are providing AngularJs training in velachery.
    For more details:AngularJs training in velachery

    ReplyDelete
  4. Needed to compose you a very little word to thank you yet again regarding the nice suggestions you’ve contributed here. Those guidelines additionally worked to become a good way to recognize that other people online have the identical fervor like mine to grasp great deal more around this condition. We are providing AngularJs training in velachery.
    For more details: AngularJs training in Velachery

    ReplyDelete
  5. I usually cant find it in me to care enough to leaves a comment for articles on the web but this was actually pretty good, thanks and keep it up, Ill check back again new york web designs

    ReplyDelete
  6. There are certainly plenty of particulars like that to take into consideration. That could be a nice level to deliver up. I offer the thoughts above as basic inspiration however clearly there are questions just like the one you bring up the place the most important thing shall be working in sincere good faith. I don?t know if finest practices have emerged around issues like that, however I am positive that your job is clearly recognized as a good game. Both girls and boys really feel the affect of just a second’s pleasure, for the remainder of their lives. website designers nyc

    ReplyDelete
  7. I like the valuable info you provide in your articles. I’ll bookmark your blog and check again here regularly. I’m quite certain I’ll learn many new stuff right here! Good luck for the next! san francisco brand agency

    ReplyDelete
  8. I conceive this site contains some rattling superb information for everyone : D. best branding agencies san francisco

    ReplyDelete