posts about

Helper Functions for Unit Testing Angular Applications

18 Oct 2014

Angular gives developers lots of great tools for unit testing their applications. As I've written more and more unit tests, I've found myself repeating the same boilerplate code in each test. To make things more succinct, I wrote some helper functions that I drop in the global namespace for all of my Angular projects. I've released all of these functions in a library called angular-test-helpers. You can install it with bower (bower install angular-test-helpers).

getService

The most common thing I find myself doing is injecting services in my tests. Here's the usual way to do it:

beforeEach(function () {
  inject(function (_$window_, _$httpBackend_, _customService_) {
    $window = _$window_;
    $httpBackend = _$httpBackend_;
    customService = _customService_;
  });
});

Here's how it looks with the getService helper function:

beforeEach(function () {
  $window = getService('$window');
  $httpBackend = getService('$httpBackend');
  customService = getService('customService');
});

And the implementation:

window.getService = function (serviceName) {
  var service;
  inject(function ($injector) {
    service = $injector.get(serviceName);
  });
  return service;
};

An unexpected benefit of getService is that it makes it less painful to isolate dependencies to the tests they're used in. When using inject, you have to write 3 lines just to get 1 service. It's very tempting to just grab all of the services you need in one huge beforeEach at the top of the test file, even if a service is only used in 1 describe block.

createDirective

I write Angular in a very directive-heavy way, so most of my unit tests end up testing directives. The way you test a directive is to create a DOM element for your directive, then manually compile the DOM using the $compile service.

At the top of literally every directive test file, I found myself writing this:

var elem;

beforeEach(function () {
  inject(function ($compile, $rootScope) {
    elem = $compile('<my-custom-directive></my-custom-directive>')($rootScope);
    $rootScope.$digest();
  });
});

Using createDirective, we can write:

var elem;

beforeEach(function () {
  elem = createDirective('<my-custom-directive></my-custom-directive>');
});

Here's the implementation:

window.createDirective = function (template) {
  var elem;
  inject(function ($compile, $rootScope) {
    elem = $compile(template)($rootScope);
    $rootScope.$digest();
  });
  return elem;
};

createDeferred

Here's a quick one for creating deferreds. Usually, the only reason you need to inject $q into your tests is to call $q.defer(), so this saves you the injection lines and gives you a shorthand for creating the deferred.

The old way:

var $q;

beforeEach(function () {
  $q = getService('$q');
});

describe('some thing', function () {
  var deferred;

  beforeEach(function () {
    deferred = $q.defer();
    ...
  });
});

With createDeferred:

describe('some thing', function () {
  var deferred;

  beforeEach(function () {
    deferred = createDeferred();
    ...
  });
});

And the implementation:

window.createDeferred = function () {
  return getService('$q').defer();
};

$rootScopeDigest

Sometimes you're testing a service that needs to crank the event loop (e.g. to force promise resolution). You don't want to have to inject $rootScope just to call $digest on it.

The old way:

var $rootScope;

beforeEach(function () {
  $rootScope = getService('$rootScope');
});

it('does a thing', function () {
  someDeferred.resolve();
  $rootScope.$digest();
});

The new way:

it('does a thing', function () {
  someDeferred.resolve();
  $rootScopeDigest();
});

The implementation:

window.$rootScopeDigest = function () {
  getService('$rootScope').$digest();
};

Conclusion

At first, I felt weird about dropping functions into the global namespace. The end result, however, has been much more succinct tests. Whenever I convert an older test file to use these helper functions, I'm able to remove code and decrease levels of indentation. I hope you find some of these functions useful, and if you have any of your own, please submit a pull request!

comments powered by Disqus