12

I have a service which contains a resource factory like this:

serviceModule.factory('ProjectResource', ['$resource', function($resource){
    return $resource('/projects/:id.json', {}, {
        'query': {method: 'GET', isArray: true}}
    );
}]);

In a form, which resides in a controller, I inject the serviceModule, and I create a new instance of the resource object:

  $scope.project = new ProjectResource({name: 'Enter a name'})

I have some issues with mocking it. I have tried creating a mock object like this, and injecting it in the controller:

 mockProjectResource = {
            query: function(){
                deferred = $q.defer();
                deferred.resolve({id: 1, :name:'test'});
                return deferred.promise;
            }
        };

No matter the unit test, I get the error:

TypeError: undefined is not a function

Which points to the initialization of the Project Resource object ($scope.project = new ProjectResource({name: 'Enter a name'})).

Are there any good way to mock the new ProjectResource(...)?

Dofs
  • 17,737
  • 28
  • 75
  • 123

4 Answers4

8

Have you had a look at 'ngMock'?

angular.module('project-resource', ['ngResource'])
    .factory('ProjectResource', function($resource){
        //Do not need to redefine the `query` action as it is defined by default by ngResource
        return $resource('/projects/:id.json', {'id': '@id'});
    });

describe('ProjectResource', function(){
    beforeEach(module('service-module-name'));

    afterEach(inject(function($httpBackend){
        //These two calls will make sure that at the end of the test, all expected http calls were made
        $httpBackend.verifyNoOutstandingExpectation();
        $httpBackend.verifyNoOutstandingRequest();
    }));

    it('mock http call', inject(function($httpBackend, ProjectResource) {
        var resource = new ProjectResource({
            id : 'abcde'
        });
        //Create an expectation for the correct url, and respond with a mock object
        $httpBackend.expectGET('/projects/abcde.json').respond(200, JSON.stringify({
            id : 'abcde',
            name : 'test'
        }));

        //Make the query
        resource.$query();

        //Because we're mocking an async action, ngMock provides a method for us to explicitly flush the request
        $httpBackend.flush();

        //Now the resource should behave as expected
        expect(resource.name).toBe('test');
    }));
});
Clark Pan
  • 6,027
  • 1
  • 22
  • 18
  • I don't see you inject a mock in the controller? I mean, you don't do something similar ctrl = $controller('projectsCntl', { $scope: scope, $http: $http, ProjectResource: mockProjectResource}). Is that the wrong way to inject a mocked service in a controller? – Dofs Jun 25 '14 at 09:08
  • I misread your question, i thought you wanted a way to mock out the AJAX calls to test your `ProjectResource` class. I'll update my answer with a how i like to inject mocked dependencies. – Clark Pan Jul 11 '14 at 00:29
7

From what I can see, I only need to create a stub object instead of a mock object.

As ProjectResource is a constructor function. You can create a stub of it like this:

mockProjectResource = function (properties){
    for(var k in properties) 
       this[k]=properties[k];
};

mockProjectResource.query = function(){
      deferred = $q.defer();
      deferred.resolve({id: 1, :name:'test'});
      return deferred.promise;
};

In case you need to know the differences between mocks and stubs: What are the differences between mocks and stubs on Rhino Mocks?. (Don't think that this link is only for Rhino mock, the concept means the same for all frameworks and technologies)

Community
  • 1
  • 1
Khanh TO
  • 48,509
  • 13
  • 99
  • 115
  • Okay, I didn't know if Angular did some magic, but a simple stub of the constructor worked. Thanks for the link. – Dofs Jun 28 '14 at 17:01
  • 2
    A shorter implementation of mockProjectResource.query is `return $q.when({id: 1, name:'test'})` – Mihnea Simian Apr 17 '15 at 09:07
1

Another way to mock a resource based service is like this:

// the name of an object should start with a capital letter
// in the case where we create a function object (as opposed to
// a literal object).
MockProjectResource = function(){};

// we add the "query" method to the "prototype" property
MockProjectResource.prototype.query = function(){
  deferred = $q.defer();
  deferred.resolve({id: 1, :name:'test'});
  return deferred.promise;
};

Mocking the query function like above works in the case where the controller expects the promise to be returned as a result of the call, e.g.:

var projectController = angular.module('projectController', []);
projectController.controller('ProjectController', ['$scope', 'ProjectResource',
  function($scope, ProjectResource) {

    var promise = ProjectResource.query();
    promise.then(function(data) {
        console.log(data);
      },
      function(err){
        console.log(err);
      });

}]);

However, if the controller expects the result to be sent in a callback, like this:

      ProjectResource.query(function(data) {
          console.log(data);
        },
        function(err){
          console.log(err);
        });

then, the service needs to be mocked like this:

MockProjectResource.prototype.query = function(success, error){
  var deferred = q.defer();
  var promise = deferred.promise;
  promise.then(success, error);
  deferred.resolve({id: 1, :name:'test'});
};    

For a full example, see my answer to a similar question:

How do you mock an angularjs $resource factory

Community
  • 1
  • 1
claudius
  • 41
  • 4
  • To see the difference between function objects and object literals look for the book "Javascript: The good parts" by Douglas Crockford, pages 20-30. The (free) PDF version of the book can be found easily by doing a Google search. – claudius Oct 29 '15 at 09:41
1

I have created a small library to help mocking ngResource: https://github.com/michalstawski/ngResourceMock

It has similar api as the $httpBackend. With it you could do something like this in your test:

$scope.project.whenQuery().resolve([]);

This will resolve every call to query with the empty array.

It is easier and much quicker then mocking the resource yourself, while at the same time beeing at the higher abstraction level than $httpBackend.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459