Mocking $resource and promises in AngularJS unit tests.

Test setup is hard, it’s tricky, and it feels like a hack. That’s how you know you’re doing it right. (maybe?)

This Post Aims to Cover

  • Why you aid testability by wrapping $resource in an api service.
  • Test setup and mocking in jasmine to handle api services.
  • How to test and resolve promises in your controller specs.

All of the code is available on plunkr

Wrap $resource in a service.

Dependency injection and single responsibility principle are crucial to testability. When you instantiate and create a $resource object in your controllers you are being detrimental to the testability of that controller because you can no longer separate the two. Isolate the functionality of your controllers away from the grunt work of the services they use. The intent of controllers is to glue data models and ui methods to the $scope. It’s so easy with angular to do everything in your controllers, but you need to resist that urge and try to recognize when you have the opportunity to refactor a dependency injectable service into your design.

Instantiation and Tight Coupling:

angular
    .module('BreakfastApp')
    .controller(
        'BreakfastCtrl',
        function($scope) {
          // ANTI-PATTERN !!!
          var BagelResource = $resource('bagels.json');
        }
     );

Dependency Injection:

angular
    .module('BreakfastApp')
    .controller(
      'BreakfastCtrl',
      function(
        $scope,
        bagelApiService
      ) {
        // bagelApiService is injected.
      }
    );

Test setup and $resource mocking

This is the controller we’re testing:

script.js

// Breakfast App
angular.module('BreakfastApp', ['ngResource']);

// Bagel Api Service
angular.module('BreakfastApp').factory(
  'bagelApiService',
  function($resource) {
    return $resource('bagels.json');
  }
);

// Breakfast Controller
angular.module('BreakfastApp').controller(
  'BreakfastCtrl',
  function($scope, bagelApiService) {
    bagelApiService
      .query()
      .$promise
      .then(function(bagelsResponse) {
        $scope.bagels = bagelsResponse;
        $scope.somethingAfterBagelsLoad = true;
      });
  }
);

This is the entire example application. It consists of one controller which loads a collection of bagels via an “api” call (a flat bagels.json file for the sake of the example). The only other thing it does is set somethingAfterBagelsLoad to true. This is a trivial thing to do after the bagels are loaded, and is again just for the purpose of exemplifying the use of promises and how to test them.

Okay, let’s have a look at them specs.

specs.js

describe('BreakfastCtrl', function() {
    var $q,
        $rootScope,
        $scope,
        mockBagelApiService,
        mockBagelsResponse;

beforeEach(module('BreakfastApp'));

beforeEach(inject(function(_$q_, _$rootScope_) {
  $q = _$q_;
  $rootScope = _$rootScope_;
}));

This loads the $q and $rootScope angular services. The inject method can handle special dependencies wrapped on either side by _ as a convenience. This allows for assigning their values to local variables named appropriately.

I need $q in order to build a promise I can return from the bagelApiService. I need $rootScope to both make a new scope for the BreakfastCtrl and to propagate promise resolutions. $q is integrated with $rootScope, read about it in the $q documentation.

beforeEach(inject(function($controller) {
  $scope = $rootScope.$new();

  mockBagelApiService = {
    query: function() {
      queryDeferred = $q.defer();
      return {$promise: queryDeferred.promise};
    }
  }

  spyOn(mockBagelApiService, 'query').andCallThrough();

  $controller('BreakfastCtrl', {
    '$scope': $scope,
    'bagelApiService': mockBagelApiService
  });
}));

This block builds the dependencies for the controller, and constructs it via the $controller service. This alters the controller’s understanding of what $scope and bagelApiService are, and replaces them with objects made locally. If you’re not familiar with unit testing, or its purpose, this can be a confusing thing to do. Why are we doing this? Why make fake objects to trick the controller? The answer is pretty simple: to isolate the subject of the test and write reliable assertions.

For instance, the mockBagelsResponse we made at the top of the file is a set of predictable values used in the tests below to make assertions. With the mock for the bagelApiService in place, those predictable values will be used to resolve the queryDeferred.promise used by mockBagelApiService.query without ever actually running any code from the real bagelApiService or sending any real requests to the “api”.

Test and resolve promises in the controller specs

describe('bagelApiService.query', function() {

    beforeEach(function() {
      queryDeferred.resolve(mockBagelsResponse);
      $rootScope.$apply();
    });`

Resolve the bagelApiService.query method’s promise with a fake, but predictable mockBagelResponse array. Propagate the resolution with $rootScope.$apply().

  it('should query the bagelApiService', function() {
    expect(mockBagelApiService.query).toHaveBeenCalled();
  });

  it('should set the response from the bagelApiServiceQuery to $scope.bagels', function() {
    expect($scope.bagels).toEqual(mockBagelsResponse);
  });

  it('should set $scope.somethingAfterBagelsLoad to true', function() {
    expect($scope.somethingAfterBagelsLoad).toBe(true);
  });
});

Those tests end up pretty simple, which is what you want. The service was called, the $scope.bagels are set, and $scope.somethingAfterBagelsLoad happened.

Thanks for reading

I sincerely hope this helps you figure out how to test your angular code. The usefulness of promises comes up a lot, and there aren’t enough in depth examples of how to write angular with $q and even fewer examples of how to test it. At Cascade Energy, much of our client code utilizes the promise api, and as a result I have a fairly large amount of experience working with and testing it. Please feel free to reach out with questions here on the blog or on twitter @nackjicholsonn if you have concerns that go beyond the bounds of this simplified example. Cheers, thanks for reading.

Again, run, fork, and play with this plunkr

Advertisements

17 thoughts on “Mocking $resource and promises in AngularJS unit tests.

  1. Thank you so much for this excellent write-up! This is exactly what I was trying to do – to mock up $promise by $resource wrapped in a factory. I just used your tip in my test spec. It works great.

  2. $resource.query returns a deferred object, so instead I added $promise to the deferred object and returned deferred, not the promise from the spy.

    Thanks for your help

  3. Thanks for the write-up, very helpful.
    If the callback is provided as follows

    function($scope, bagelApiService) {
    bagelApiService.query(function(bagelsResponse) {
    $scope.bagels = bagelsResponse;
    $scope.somethingAfterBagelsLoad = true;
    });
    }

    (that is, without using $promise.then() )

    what should the mockBagelApiService return?
    I have forked your plunk and experimented but can’t get the callback to be called during the test, when providing the callback as above…

    thanks again for the write-up

  4. Thanks very much for this. Very useful. Took me a while to get it working. In case anyone else is as slow as I, what held me up was that I was testing a module, not a full app, and the dependency on ui.router was in the main app definition not the module under test. Hence I kept getting:

    Failed to instantiate module xyz due to:
    Unknown provider: $stateProvider

    Adding the ui.router dependency to xyz’s declaration fixed it.

  5. I keep getting this error:

    ReferenceError: queryDeferred is not defined

    I’m not sure how queryDeferred works since it’s defined in the scope of the mock service.

  6. I’m with Victor here… how is this supposed to work with queryDeferred undefined? I’m not sure how it’s working in plunkr, but when I try to recreate it in my own code, it fails.

  7. Victor, Ryan
    queryDeferred works because because of strict mod violation.
    Author doesn’t use “use strict” in his script – that is why queryDeferred has been assigned to global window object and accessible in any scope.

  8. The spyOn(…).andCallThrough() call does not work in newer versions of Jasmine. It should be replaced with spyOn(…).and.callThrough().

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s