Search code examples
javascriptangularjsunit-testingjasminespy

Jasmine: Real function being called after spyOn with function reference


I have an angular service. Inside this service I have an object with a function, that references another function on the service. (Code below)

I want to use Jasmine (1.3) to spy on my service function, to verify that when the object's function gets called, it actually calls the real function.

My problem: After calling spyOn, the real function is still being called.

FooService.js

angular.module('foo').service("FooService", function() {
     var self = this;

     this.fooFunction = function() {
         console.log("Foo function is being called");
     }

     this.bar = {
         barFunction : self.fooFunction
     }
});

FooService-spec.js

describe("Testing FooService", function() {
     var service;

     beforeEach(inject(function(_FooService_) {
         service = _FooService_;
     }));

     describe("Test bar object", function() {
        it("should call fooFunction when bar.barFunction is called", function() {
             spyOn(service, "fooFunction");
             service.bar.barFunction();
             expect(service.fooFunction).toHaveBeenCalled();
         });
     });
 });

I have found that if I change FooServce.js to the following, this all works though:

FooService - Working

angular.module('foo').service("FooService", function() {
     var self = this;

     this.fooFunction = function() {
         console.log("Real function is being called");
     }

     this.bar = {
         barFunction : function() {
             return self.fooFunction();
         }
     }
 });

What part of JavaScript / Angular / Jasmine am I failing to understand in the first example?


Solution

  • spyOn operates by replacing the value of an object property with a different value. When you do spyOn(service, "fooFunction"); you're doing something like

    var realFunc = service.fooFunction;
    service.fooFunction = function() {
        doSpyStuff();
        return realFunc.apply(this, arguments);
    }
    

    Note that this doesn't modify the value service.fooFunction. It actually modifies service -- namely, one of the properties of service is now a totally different function. This replacement will only affect the fooFunction property of service. If you're not accessing a property of service, you're certainly not on your way to invoking the spy function.

    So let's apply this knowledge to your case. In your test, you are accessing a property of service.bar. While service.bar.barFunction and service.fooFunction were originally the same value, service has had its fooFunction property replaced by the spy, while (very importantly) service.bar has not had any of its properties mutated by spyOn. When you call service.bar.barFunction(), you directly invoke the real function, and have no connection to the spy that lives on service's fooFunction property.

    By contrast, when you do barFunction: function() { return self.fooFunction(); } as an anonymous function, you actually are accessing the spy property value on service, because here self happens to be service, so self.fooFunction is service.fooFunction, which is the property that holds a spy replacement value.