Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save nackjicholson/3ab3ce9ced1b70434421 to your computer and use it in GitHub Desktop.
Save nackjicholson/3ab3ce9ced1b70434421 to your computer and use it in GitHub Desktop.
Mocking Promises and Resources in AngularJS

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

@NishiBangar
Copy link

NishiBangar commented Aug 31, 2016

Hi Nackjicholson, flawless information. As a matter of fact, i have a small clarification where i am struck on .
The isolated "resource factory(ie bagelServiceAPI) returns the instantiated "resource Object" for a static url (Particularly, only one request in your example), So, hence, it is possible to replace the original "Service API" with that of the "locally" created one in the test (ie. mockBagelServiceAPI), thus, mocking the implementation of ".query()" within the test.... Which is great actually, but only of a single request.

Now, my question is, what if there are multiple "http request" that needs to be done through "$resource Object for different URLs, to be specific", which eventually makes use of the same "Serivce API (ie, bagerServiceAPI)..?
In Other words, i just want my "Service API" to be reusable , so that "resource objects " can be instantiated and acquired for different "URLs"..

Would really appreciate if you could help me out with this, (will admire , if suitable examples provided along)..
Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment