Mock a service in order to test a controller

52,103

Solution 1

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();
    });
});

Solution 2

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();
});

Solution 3

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

Share:
52,103
Javier Hertfelder
Author by

Javier Hertfelder

I am a Software engineer with more than 9 years of experience. In the last 4 years, I have assisted FXStreet - a leading Forex market information website - with the attainment of more than 4 million page views every month to recover from an unexpected “hiccup” 90% of the IT team left in less than 2 months. I was hired to accomplish two goals. Firstly, to rebuild the whole department from scratch. The second aim was to lead the reformation of the whole FXStreet Website and their 15 year old monolithic architecture. We switched from a monolithic to a microservices oriented architecture easy to maintain, monitor and develop. After one year and a half, thanks to an exceptional group of engineers, we achieved it, not without pain, not without effort. We now have a solid department where everybody has a voice, where everybody shares information and where everybody enjoys agile development.

Updated on August 23, 2022

Comments

  • Javier Hertfelder
    Javier Hertfelder almost 2 years

    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?

  • Javier Hertfelder
    Javier Hertfelder over 11 years
    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..
  • AA.
    AA. over 10 years
    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.