42

I have a ParseService, that I would like to mock in order test all the controllers that are using it, I have been reading about jasmine spies but it is still unclear for me. Could anybody give me an example of how to mock a custom service and use it in the Controller test?

Right now I have a Controller that uses a Service to insert a book:

BookCrossingApp.controller('AddBookCtrl', function ($scope, DataService, $location) {

    $scope.registerNewBook = function (book) {
        DataService.registerBook(book, function (isResult, result) {

            $scope.$apply(function () {
                $scope.registerResult = isResult ? "Success" : result;
            });
            if (isResult) {
                //$scope.registerResult = "Success";
                $location.path('/main');
            }
            else {
                $scope.registerResult = "Fail!";
                //$location.path('/');
            }

        });
    };
});

The service is like this:

angular.module('DataServices', [])

    /**
     * Parse Service
     * Use Parse.com as a back-end for the application.
     */
    .factory('ParseService', function () {
        var ParseService = {
            name: "Parse",

            registerBook: function registerBook(bookk, callback) {

                var book = new Book();

                book.set("title", bookk.title);
                book.set("description", bookk.Description);
                book.set("registrationId", bookk.RegistrationId);
                var newAcl = new Parse.ACL(Parse.User.current());
                newAcl.setPublicReadAccess(true);
                book.setACL(newAcl);

                book.save(null, {
                    success: function (book) {
                        // The object was saved successfully.
                        callback(true, null);
                    },
                    error: function (book, error) {
                        // The save failed.
                        // error is a Parse.Error with an error code and description.
                        callback(false, error);
                    }
                });
            }
        };

        return ParseService;
    });

And my test so far look like this:

describe('Controller: AddBookCtrl', function() {

    //  // load the controller's module
    beforeEach(module('BookCrossingApp'));


    var AddBookCtrl, scope, book;

    // Initialize the controller and a mock scope
    beforeEach(inject(function($controller, $rootScope) {
        scope = $rootScope;
        book = {title: "fooTitle13"};
        AddBookCtrl = $controller('AddBookCtrl', {
            $scope: scope
        });
    }));

    it('should call Parse Service method', function () {

        //We need to get the injector from angular
        var $injector = angular.injector([ 'DataServices' ]);
        //We get the service from the injector that we have called
        var mockService = $injector.get( 'ParseService' );
        mockService.registerBook = jasmine.createSpy("registerBook");
        scope.registerNewBook(book);
        //With this call we SPY the method registerBook of our mockservice
        //we have to make sure that the register book have been called after the call of our Controller
        expect(mockService.registerBook).toHaveBeenCalled();
    });
    it('Dummy test', function () {
        expect(true).toBe(true);
    });
});

Right now the test is failing:

   Expected spy registerBook to have been called.
   Error: Expected spy registerBook to have been called.

What I am doing wrong?

Venugopal
  • 1,888
  • 1
  • 17
  • 30
Javier Hertfelder
  • 2,432
  • 4
  • 22
  • 36

4 Answers4

61

What I was doing wrong is not injecting the Mocked Service into the controller in the beforeEach:

describe('Controller: AddBookCtrl', function() {

    var scope;
    var ParseServiceMock;
    var AddBookCtrl;

    // load the controller's module
    beforeEach(module('BookCrossingApp'));

    // define the mock Parse service
    beforeEach(function() {
        ParseServiceMock = {
            registerBook: function(book) {},
            getBookRegistrationId: function() {}
       };
   });

   // inject the required services and instantiate the controller
   beforeEach(inject(function($rootScope, $controller) {
       scope = $rootScope.$new();
       AddBookCtrl = $controller('AddBookCtrl', {
           $scope: scope,
           DataService: ParseServiceMock
       });
   }));

   it('should call registerBook Parse Service method', function () {
       var book = {title: "fooTitle"}

       spyOn(ParseServiceMock, 'registerBook').andCallThrough();
       //spyOn(ParseServiceMock, 'getBookRegistrationId').andCallThrough();
       scope.registerNewBook(book);

       expect(ParseServiceMock.registerBook).toHaveBeenCalled();
       //expect(ParseServiceMock.getBookRegistrationId).toHaveBeenCalled();
    });
});
isherwood
  • 58,414
  • 16
  • 114
  • 157
Javier Hertfelder
  • 2,432
  • 4
  • 22
  • 36
11

You can inject your service and then use spyOn.and.returnValue() like this:

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

beforeEach(angular.mock.inject(function($rootScope, $controller, ParseService) {
    mock = {
        $scope: $rootScope.$new(),
        ParseService: ParseService
    };
    $controller('AddBookCtrl', mock);
}));

it('should call Parse Service method', function () {
    spyOn(mock.ParseService, "registerBook").and.returnValue({id: 3});

    mock.$scope.registerNewBook();

    expect(mock.ParseService.registerBook).toHaveBeenCalled();
});
dduft
  • 470
  • 8
  • 18
2

Following Javito's answer 4 years after-the-fact. Jasmine changed their syntax in 2.0 for calling through to real methods on spies.

Change:

spyOn(ParseServiceMock, 'registerBook').andCallThrough();

to:

spyOn(ParseServiceMock, 'registerBook').and.callThrough();

Source

Petter Friberg
  • 21,252
  • 9
  • 60
  • 109
Steve
  • 538
  • 4
  • 17
-2

Include angular-mocks.js in your project and read carefully through the following link.

remigio
  • 4,101
  • 1
  • 26
  • 28
  • 1
    The problem is that Parse service encapsulates the http calls, so I don't see how to use httpBackend mock in my app, maybe I am missing the point here and I always must use httpBackend to test this kind of services.. – Javier Hertfelder Apr 08 '13 at 08:19
  • 1
    I think your approach is good. The httpBackend should be used to test ParseService, not to test the controller. If you mock the calls to http from controller then you're violating the encapsulation. You could to add a integration test to test the true call to external server. – AA. Apr 02 '14 at 23:19