1

In my angular JS application I have a mainController which takes in a userFactory as a parameter. The userFactory consists of an object called userService which in turn has a userDetails object and some methods including resetUserDetails. (see further down)

In the mainController I have a logOut function which calls the userFactory.userService.resetUserDetails method. I would like to test this logOut function with jasmine however I am getting some errors. I am very new to Jasmine so apologies if its something obvious i'm missing.

So firstly in my Jasmine suite I create a MainControllerSpec for testing my mainController.

Within this spec I'm injecting a factory called userFactory. I'm trying to spyOn my resetUserDetails method as follows however getting an error:

spyOn(userFactory, 'userService.resetUserDetails');

Error: userService.resetUserDetails() does not exist.

I tried this process by creating a function called test in my userFactory (outside the userService object) and it works well so at least I know the factory injection in the spec is set up fine.
Any help greatly appreciated. Thanks

MainControllerSpec.js

describe("MainController", function () { 
    beforeEach(angular.mock.module('mapModule', 'ngRoute','ngTouch', 'ngAnimate'));
    var scope, userFactory; 

    beforeEach(inject(function($rootScope, $controller, _userFactory_){
        scope = $rootScope.$new();
        userFactory = _userFactory_;
        $controller('mainController', {
            $scope: scope
        }); 
    }));


   describe('The logOut function', function() {
        it('should call the resetUserDetails function of the userFactory.userService object and reset the userDetails object', function() {
            //spyOn takes in a factory and a method of that factory 
            spyOn(userFactory, 'userService.resetUserDetails');
            //spyOn(userFactory, 'test'); tried this and it works.
            scope.logOut();
            expect(userFactory.userService.resetUserDetails).toHaveBeenCalled();  
        });
    });

});

logOut function in mainController

   $scope.logOut = function(){
         userFactory.userService.resetUserDetails(); 
         //userFactory.test(); //tried this with spyOn in jasmine
    }

userFactory

mapApp.factory('userFactory', function(){

    var userService = {
        /*
         * Initialize a userDetails object. 
         */
        userDetails : {   
            "userID" : null,
            "facebookUserID" : "",
            "facebookName" : "",
            "facebookProfilePic" : "",
            "userPrivilegeID" : 1,
            "userToken" : "",
            "isLoggedIn" : false
        },
        resetUserDetails : function(){
            /*
             * This method resets the userDetails object.
             */
            this.userDetails = {
                "userID" : null,
                "facebookUserID" : "",
                "facebookName" : "",
                "facebookProfilePic" : "",
                "userPrivilegeID" : 1,
                "userToken" : "",
               "isLoggedIn" : false
            };
        }
    }; 
    var test = function(){
        /*
        * for testing spyOn in Jasmine
        */
    };
    //return public API so that we can access it in all controllers
    return{
      userService: userService,
      test: test
    };
});
Sarah
  • 1,943
  • 2
  • 24
  • 39
  • Ive just copped it. apologies. The following works fine. spyOn(userFactory.userService, 'resetUserDetails'); .. – Sarah Oct 08 '17 at 15:28
  • Usually you may want to mock service entirely in controller tests instead of spying on its methods one by one. E.g. https://stackoverflow.com/a/46595428/3731501 – Estus Flask Oct 08 '17 at 16:03
  • @estus ok thanks for the advice. I'll check that out – Sarah Oct 08 '17 at 16:34
  • @estus Thanks so in my case would I do something like this to spy on the factory (in global scope): beforeEach(function() { module('mapModule', { userFactory: { userService: jasmine.createSpy() } }); }); and then in the suite: describe('The logOut function', function() {... should I leave out the spyOn line? I tried this but it didnt work. Thanks for the help. I'm new to jasmine – Sarah Oct 08 '17 at 17:11
  • userService shouldn't be a spy. It is an object that contains `resetUserDetails` method. Your case is exactly the same as in linked answer. spyOn isn't needed if a function is already a spy. Instead, spy returned value should be set with `.and.returnValue(...)` (if necessary). – Estus Flask Oct 08 '17 at 17:17
  • @estus ah ok I got it now. I had to put it in the second line of my spec file: beforeEach(angular.mock.module('mapModule', { userFactory: { userService: { resetUserDetails: jasmine.createSpy() }} }, 'ngRoute','ngTouch', 'ngAnimate')); ..thanks – Sarah Oct 08 '17 at 17:31
  • It's not the subject, but btw, using real modules (ngRoute, etc) is the last thing you want to do in unit tests. They break test isolation and add unnecessary moving parts. – Estus Flask Oct 08 '17 at 17:49
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/156217/discussion-between-sarah-and-estus). – Sarah Oct 08 '17 at 17:51

1 Answers1

1

You need to mock your userFactory before to inject it directly. Goals of unit tests are to test the files as black boxes, without testing also the logic of the related methods directly.

For them you will write your spec file for the userFactory instead.

In this case what you can do is something like the following:

describe("MainController", function() {

  beforeEach(angular.mock.module('mapModule', 'ngRoute', 'ngTouch', 'ngAnimate'));
  var scope, userFactory;

  // here mock the methods of your factory
  beforeEach(module(function($provide) {
    $provide.value('userFactory', {
      myFirstObject: {
        myFirstMethod: function() {}
      }
    });
  }));

  beforeEach(inject(function($rootScope, $controller, _userFactory_) {
    scope = $rootScope.$new();
    userFactory = _userFactory_;
    $controller('mainController', {
      $scope: scope
    });
  }));


  describe('The logOut function', function() {
    it('should call the resetUserDetails function of the userFactory.userService object and reset the userDetails object', function() {
      //here spy on the method and return what you would like to return in this test
      // or if you don't need to manage the return, as it seems you don't, just use callThrough
      spyOn(userFactory.myFirstObject, 'myFirstMethod').and.callThrough();
      scope.logOut();
      expect(userFactory.myFirstObject.myFirstMethod).toHaveBeenCalled();
    });
  });

});
quirimmo
  • 9,800
  • 3
  • 30
  • 45
  • Thank you. I'll try this out tomorrow. Although I fixed my own code as you can see in the comments above. I needed to say this: spyOn(userFactory.userService, 'resetUserDetails'); instead of this: spyOn(userFactory, 'userService.resetUserDetails'); ..(because my method is within an object called userService within the userFactory)..but i appreciate your advice and will try what you suggested. thanks – Sarah Oct 08 '17 at 21:02
  • oh sorry didn't notice your method was inside the nested object, changed the code in the answer – quirimmo Oct 09 '17 at 04:45
  • thank you . I tried this and its working well thanks. However an extra thing I did (with thanks to another person in the comments). I created a spy using jasmine.createSpy() so $provide.value('userFactory', { myFirstObject: { myFirstMethod: jasmine.createSpy() } }); and then I didnt need to use spyOn within the test in the suite but continued with the rest of the code. Anyway you're answer is correct for my question so I'll accept it now. thank you very much – Sarah Oct 09 '17 at 09:54
  • yeah it is valid absolutely also in that way, you can do the spy directly there, but if in future tests you need the spy just on some test, before you need to mock the function, you need to start the definition normally and then `spyOn` – quirimmo Oct 09 '17 at 16:29