Testing $resource services in AngularJS
Asked Answered
C

2

38

I am trying to begin writing unit tests for my angular application and hit a stopping block pretty quick as I am unsure of how exactly to mock my service in a testable way.
Is there a way to mock the REST call otherwise it would seem like I need to mirror everything within my service in my tests which doesn't seem right to me, but I am rather new to test writing so maybe this is how it is supposed to be accomplished. Any help would be greatly appreciated.

My service is as follows:

angular.module('resources.users', ['ngResource'])
.factory('User', function($resource) {
   var resource = $resource('/api/index.php/users/:username', {}, {
      'update': {method: 'PUT'}
   });

   resource.getUser = function(username, successCb) {
      return resource.query({username: username}, successCb);
   };

   return resource;
});

My test consists thus far of:

describe('User', function() {
    var mockUserResource;
    beforeEach(module('resources.users'));
    beforeEach(function() {
        mockUserResource = sinon.stub({
            getUser: function(username) {
                mockUserResource.query({username: username});
            },
            query: function() {}
        });
        module(function($provide) {
            $provide.value('User', mockUserResource);
        })
   });
   describe('getUser', function() {
      it('should call getUser with username', inject(function(User) {
          User.getUser('test');
          expect(mockUserResource.query.args[0][0]).toEqual({username: 'test'});
      }));
   })
});
Consultation answered 15/8, 2013 at 17:44 Comment(2)
Can you explain sinon.stub?Absorbance
I used sinon originally to stub out functions but have since not used it as it doesn't seem to be needed. ( sinonjs.org )Consultation
R
54

You can mock the requests made by ngResource like this:

describe('User', function () {
    var mockUserResource, $httpBackend;
    beforeEach(angular.mock.module('myApp'));

    beforeEach(function () {
        angular.mock.inject(function ($injector) {
            $httpBackend = $injector.get('$httpBackend');
            mockUserResource = $injector.get('User');
        })
    });

    describe('getUser', function () {
        it('should call getUser with username', inject(function (User) {
            $httpBackend.expectGET('/api/index.php/users/test')
                .respond([{
                username: 'test'
            }]);

            var result = mockUserResource.getUser('test');

            $httpBackend.flush();

            expect(result[0].username).toEqual('test');
        }));

    });
});

Demo

Rennarennane answered 15/8, 2013 at 19:17 Comment(7)
Awesome! What would I do without you for all my angular questions sza?Consultation
@Consultation It is always good to figure out something together. :)Rennarennane
@sza +1 for this. I have a similiar question using JSONP in something a little more recent (Angular 1.2 and Karma 0.10.2) #19096800Rhomboid
The only issue with the test itself is that the value been asserted is the mocked data. result[0].username always will have the value of 'test' so it's kind of pointless. This test should be verifying if the $scope has been affected by the result.Turbulent
I get "Unexpected request: GET "../../someTemplateURL.html" no more request expected if I add $httpBackend.flush(). Any ideas?Listen
It's really bizarre that it's barfing on a request unrelated to my $resource.query(). and it only does this with flush command.Listen
Great response. :)Misogynist
R
31

zsong's answer greatly helped me understand this, but I would like to expand on how it works. In case it gets edited, I list the code again here:

describe('User', function () {
    var mockUserResource, $httpBackend;
    beforeEach(angular.mock.module('myApp'));

    beforeEach(function () {
        angular.mock.inject(function ($injector) {
            $httpBackend = $injector.get('$httpBackend');
            mockUserResource = $injector.get('User');
        })
    });

    describe('getUser', function () {
        it('should call getUser with username', inject(function (User) {
            $httpBackend.expectGET('/api/index.php/users/test')
                .respond([{
                username: 'test'
            }]);

            var result = mockUserResource.getUser('test');

            $httpBackend.flush();

            expect(result[0].username).toEqual('test');
        }));

    });
});

What's going on here?

1

beforeEach(angular.mock.module('myApp'));

We tell the Angular injector ($injector and angular.mock.inject) to inject things defined in the myApp module. You can think of it as defining a module dependency without a dependent module. Compare with how things defined in the myApp module can be injected in, say, a controller in a angular.module('myOtherApp', ['myApp']) module.

2

beforeEach(function () {
    angular.mock.inject(function ($injector) {
        $httpBackend = $injector.get('$httpBackend');
        mockUserResource = $injector.get('User');
    })
});

Before each spec, run the function ($injector) function with dependencies injected. In this case, the dependency ($injector) is resolved implicitly from the parameter name. A functionally equivalent variant of this snippet is

beforeEach(function () {
    angular.mock.inject(['$httpBackend', 'User', function ($httpB, User) {
        $httpBackend = $httpB;
        mockUserResource = User;
    }]);
});

Here we have instead declared the dependencies explicitly, and are free to use any parameter names we wish.

3

it('should call getUser with username', inject(function (User) {

Again, the test function is injected with the implicitly resolved User service as a parameter, though it isn't actually used.

Notice that this time there is no wrapper function around the inject call. inject invokes the passed function immediately if a spec is currently running, but otherwise it returns a wrapper function (see the inject docs and source code), so we don't actually need the wrapper function. Thus, we could have written the beforeEach snippet above like this:

beforeEach(angular.mock.inject(function ($injector) {
    $httpBackend = $injector.get('$httpBackend');
    mockUserResource = $injector.get('User');
}));
Recur answered 5/8, 2014 at 15:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.