Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Unit Testing - Mocking methods/closures in same service

I'm currently trying to unit test my angular service using SinonJS, but have been running into an issue and was hoping someone could possibly shed some light on why this is occurring. I have reconstructed my current project to illustrate the problem at hand.

I have also provided a DEMO

I have a service, peopleService:

(function (){

  angular.module('myApp')
    .factory('peopleService', peopleService);

    peopleService.$inject = ['$q'];

    function peopleService ($q){
      var people = ['Homer', 'Marge', 'Bart', 'Lisa', 'Maggie'];

      // in actual project, this makes an http request
      function getFamily () {
         return people;
      }

      function getAdults (){
        var family = getFamily();
        return family.filter(function (person){
          return person === 'Homer' || person === 'Marge';
        });
      }

      return {
        getFamily: getFamily,
        getAdults: getAdults
      }
    }

}());

In this service, my method getAdults uses getFamily, filters the results, and returns the data.

In my unit test, I am trying to mock getFamily and see if that method is being called. Now this is where the problem presents itself...

First thing I tried was stubbing out the method and overwriting the current method, like so:

beforeEach(function (){
  module('myApp');

  inject(function (_peopleService_){
    peopleService = _peopleService_; // get the service
    sinon.stub(peopleService, 'getFamily'); // stub it
  });
});

I then go to test whether getAdults calls the getFamily method:

it('getAdults should call "getFamily" once', function(){
     peopleService.getAdults();
     expect(peopleService.getFamily.calledOnce).toBe(true);
});

The test fails and the stubbed method is not called...

I debug and find out that although the function has in fact changed: enter image description here

The service still holds a reference (closure) to what the method used to be when the service was created:

enter image description here

My initial thought was I didn't stub the method correctly. I then attempted overwriting the method using $provide ($provide.value) as well as $injector decorator and I ended up getting the same result (closure held onto the original method).

A solution to this would be to use this:

 function getAdults (){
    var family = this.getFamily(); // <-- by using this.getFamily would reference the mock
    return family.filter(function (person){
      return person === 'Homer' || person === 'Marge';
    });
  }

However, I do not understand why I have to do this.

In short, does anyone know:

  • how to mock another method in the same service without having to use this
  • how to mock out a closure variable in a unit test

Thank you so much for your time.

like image 522
Dom Avatar asked Dec 06 '14 04:12

Dom


1 Answers

When you stub a method on an object, the property of that object is overriden, not the original function that it references.

Take, for example, this code:

function myFunction () {};
var myObj = { prop: myFunction };

myObj.prop === myFunction; // true
myObj.prop = 'changed';

typeof myFunction === 'function'; // true
myObj.prop === myFunction; // false

Changing myObj.prop did not change the original function, myFunction still exists in its own right. myObj.prop, however, has lost its reference to myFunction. If this was in sinon world, stubbing simply changed the reference of myObj.prop to a stub object.

This is why when testing code in a service which calls another function in the same service, that code needs to reference the same object returned by the service. If you want to avoid using the this keyword everywhere, you can structure your service like so:

angular.module('myApp')
  .factory('peopleService', peopleService);

peopleService.$inject = ['$q'];

function peopleService ($q){
  var service = {
    getFamily: getFamily,
    getAdults: getAdults
  };

  return service;

  function getFamily () {
     // ...
  }

  function getAdults (){
    var family = service.getFamily(); // <-- reference service.getFamily()
    // ...
  }
}
like image 70
user2943490 Avatar answered Oct 05 '22 03:10

user2943490