MatTaNg
MatTaNg

Reputation: 895

Jasmine spyOn does not mock function correctly

So I have a function like so:

        function initializeView() {
            var deferred = $q.defer();
            var deleteOrUploadToBestMoments = this.deleteOrUploadToBestMoments;
            var checkAndDeleteExpiredMoments = this.checkAndDeleteExpiredMoments;

            getNearbyMoments()
            .then(deleteOrUploadToBestMoments)
            .then(checkAndDeleteExpiredMoments).then(function(moments) {
                //omitted
                }, function(error) {
                    deferred.reject(error);
                });
            return deferred.promise;
        };

I wouldl ike to test this function with a unit test. Here is my test:

it('Should call initializeView', function(done) {
    spyOn(service, 'getNearbyMoments').and.callFake(function() {
        console.log("MOCKED getNearbyMoments");
        return $q.resolve(mockOutMoments());
    });
    spyOn(service, 'deleteOrUploadToBestMoments').and.callFake(function() {
        console.log("MOCKED deleteOrUploadToBestMoments");
        return $q.resolve();
    });
    spyOn(service, 'checkAndDeleteExpiredMoments').and.callFake(function() {
        console.log("MOCKED checkAndDeleteExpiredMoments");
        return $q.resolve();
    });
    service.initializeView().then(function(moments) {
        //omitted
        done();
    });
    $scope.$apply();
});

The problem is with the spyOn functions. It is not mocking the correct functions. However, it does mock it out correctly if I add a 'this' in front of getNearbyMoments in my production code like so:

   function initializeView() {
        var deferred = $q.defer();
        var deleteOrUploadToBestMoments = this.deleteOrUploadToBestMoments;
        var checkAndDeleteExpiredMoments = this.checkAndDeleteExpiredMoments;

        this.getNearbyMoments()
        .then(deleteOrUploadToBestMoments)
        .then(checkAndDeleteExpiredMoments).then(function(moments) {
            //omitted
            }, function(error) {
                deferred.reject(error);
            });
        return deferred.promise;
    };

I could just add a 'this' in front of all my function calls in my production code but this becomes a problem when the context of 'this' changes. Also, I shouldn't need to change my production code just to make my tests happy - Which makes me really want to figure out what I am doing wrong in my tests so I don't put a band-aid on a potential huge problem.

EDIT:

Apparently I was doing it correctly using 'this'. But the problem is that whenever I do that I get an error saying

'TypeError: this.getNearbyMoments is not a function`

Heres my code:

function initializeView() {
            var deferred = $q.defer();
            var deleteOrUploadToBestMoments = this.deleteOrUploadToBestMoments;
            var checkAndDeleteExpiredMoments = this.checkAndDeleteExpiredMoments;
            var getNearbyMoments = this.getNearbyMoments;
            this.getNearbyMoments()
            .then(deleteOrUploadToBestMoments)
            .then(checkAndDeleteExpiredMoments).then(function(moments) {
                //omitted
                }, function(error) {
                    console.log("ERROR");
                    console.log(error);
                    deferred.reject(error);
                });
            return deferred.promise;
        };

For some context my service looks something like this:

(function() {
    angular.module('app.momentsService', [])

    .service('momentsService', ['core', '$q', 'constants', 'logger', 'geolocation', 'awsServices', 'localStorageManager', momentsService]);

    function momentsService(core, $q, constants, logger, geolocation, awsService, localStorageManager){

//Lots of other functions
        this.checkAndDeleteExpiredMoments = checkAndDeleteExpiredMoments;
        this.getNearbyMoments = getNearbyMoments;
        this.deleteOrUploadToBestMoments = deleteOrUploadToBestMoments;

        function getNearbyMoments() {
//Omitted...
       };
function initializeView() {
//Well, you already know whats in here...
};

So I'm confused because I clearly define the function at the top of the service but for some reason it does not know that it exists.

Upvotes: 2

Views: 4384

Answers (4)

Pratheep
Pratheep

Reputation: 1076

If you do

this.getNearbyMoments()...

inside initializeView(), you need to change

function getNearbyMoments() {
  //Omitted...
};

to

this.getNearbyMoments = function() {
  //Omitted...
};

Upvotes: 0

sab
sab

Reputation: 5022

what about a little that=this?

in the code ... that=the context that.getNearbyMoments() .then(deleteOrUploadToBestMoments)

and in the test:

...
 that=service
 service.initializeView()

Upvotes: 0

tandrewnichols
tandrewnichols

Reputation: 3466

Your code doesn't show this, but I'm assuming maybe you're doing something like

var getNearbyMoments = this.getNearbyMoments;

somewhere, since you're trying to spy on service.getNearbyMoments, but not actually calling a function by that name that's attached to service. If that's the case, my guess is that this is a javascript reference issue. spyOn works something like this (but more complicated):

function(obj, method) {
  var originalFn = obj[ method ];
  obj[ method ] = function() {
    // do spy stuff
    // and potentially call the original
    // depending what you do with the spy
    originalFn()
  }
}

Because you've assigned the original function to it's own variable, when that function is wrapped by spyOn, it no longer points to the same reference.

You either need to use this (or service) or spy on the function earlier before it's reassigned to a local variable. Again, not seeing more of the code makes this difficult to assess.

For proof of this reference issue, try:

var obj = {
  foo: function() {}
};
var foo = obj.foo;
spyOn(obj, 'foo')
foo();
expect(obj.foo).to.haveBeenCalled() // -> false

As for the issue of this being different . . . I always define services like this:

angular.module('app').factory('SomeService', function() {
  var service = {
    foo: function() {
      service.bar();
    },

    bar: function() {
      console.log('bar');
    }
  };
  return service;
});

that way everything in the service itself uses a local reference to itself, which never changes.

Upvotes: 0

DoctorPangloss
DoctorPangloss

Reputation: 3073

spyOn(service, 'getNearbyMoments').and.callFake(function() { console.log("MOCKED getNearbyMoments"); return $q.resolve(mockOutMoments()); });

Due to the mechanics of spyOn, it's not possible to modify a call to getNearbyMoments(). The reference inside the initializeView function cannot be overwritten by spyOn in that particular way.

You'll have to add this in front of getNearbyMoments as you've discovered in order to use spyOn.

Otherwise, you can implement the behavior you want by making a publicly exposed reference to getNearbyMoments and modifying it. But you'd basically be reimplementing an entire mocking system and doing something more complex than adding this.

Upvotes: 1

Related Questions