11

I have a method in a service that uses underscore's debounce.

Inside that method is a call to a method on a different service. I'm trying to test that the different service is called.

In my attempts to test the debounced method, the different services' method is never called, and jasmine fails with:

"Expected spy aMethod to have been called."

I know for a fact that it IS called (it logs to console in chrome), it's just called AFTER the expectation already failed.

So... (preferably) without adding Sinon or another dependency and with
bonus points* given to a solution doesn't have to turn the _.debounce into a $timeout...

How do?

angular.module('derp', [])
.service('herp', function(){ 
   return {
     aMethod: function(){ 
       console.log('called!'); 
       return 'blown'; 
     }
   }; 
 })
 .service('Whoa', ['herp', function(herp){
   function Whoa(){
     var that = this;
     this.mindStatus = 'meh';
     this.getMind = _.debounce(function(){
       that.mindStatus = herp.aMethod();
     }, 300);
   }
   return Whoa;
 }]);

tests:

describe('Whoa', function(){
  var $injector, whoa, herp;

  beforeEach(function(){
    module('derp');
    inject(function(_$injector_){
      var Whoa;
      $injector = _$injector_;
      Whoa = $injector.get('Whoa');
      herp = $injector.get('herp');
      whoa = new Whoa();
    });
  });

  beforeEach(function(){
    spyOn(herp, 'aMethod').andCallThrough();
  });

  it('has a method getMind, that calls herp.aMethod', function(){
    whoa.getMind();
    expect(herp.aMethod).toHaveBeenCalled();
  });
});

Why have the AngularJS Testing gods forsaken me?

* I do not know how to give actual bonus points on stackoverflow, but if it is possible, i will.

mdewitt
  • 2,526
  • 19
  • 23
Andrew Luhring
  • 1,762
  • 1
  • 16
  • 37

3 Answers3

15

You just need to mock lodash debounce method:

describe('Whoa', function(){
  var $injector, whoa, herp;

  beforeEach(function(){
    module('derp');
    spyOn(_, 'debounce').and.callFake(function(cb) { return function() { cb(); } });
    inject(function(_$injector_){
      var Whoa;
      $injector = _$injector_;
      Whoa = $injector.get('Whoa');
      herp = $injector.get('herp');
      whoa = new Whoa();
    });
  });

  beforeEach(function(){
    spyOn(herp, 'aMethod').andCallThrough();
  });

  it('has a method getMind, that calls herp.aMethod', function(){
    whoa.getMind();
    expect(herp.aMethod).toHaveBeenCalled();
  });
});
Christophe Geers
  • 8,564
  • 3
  • 37
  • 53
Wawy
  • 6,259
  • 2
  • 23
  • 23
  • Nope. Still not working. In both of our cases herp.aMethod IS called, but neither are called before the expectation. – Andrew Luhring Dec 28 '15 at 22:56
  • I wish I had chai as promised right now. an "eventually" would fix this. – Andrew Luhring Dec 28 '15 at 22:57
  • put a break point inside the anonymous function inside the callfake and check the stack to see where it is called from. – Wawy Dec 28 '15 at 22:59
  • Is the mock function actually getting called? – Wawy Dec 28 '15 at 23:08
  • no idea why, but i couldn't get your _.debounce to be called. at the end of the day i ended up using [angular debounce](https://github.com/shahata/angular-debounce) so i could use $timeout.flush() in my tests. – Andrew Luhring Jan 06 '16 at 23:27
  • 1
    @AndrewLuhring I've edited my answer. I realise now why it didn't work, the spy was being done after the injector instantiated your service so it was too late. Now, it should work. – Wawy Jan 07 '16 at 17:06
3

Angular $timeout has advantage in tests because it is mocked in tests to be synchronous. The one won't have this advantage when third-party asynchronous tools one used. In general asynchronous specs will look like that:

var maxDelay = 500;

  ...
  it('has a method getMind, that calls herp.aMethod', function (done){
    whoa.getMind();
    setTimeout(function () {
      expect(herp.aMethod).toHaveBeenCalled();
      done();
    }, maxDelay);
  });

Since Underscore debounce doesn't offer flush functionality (while the recent version of Lodash debounce does), asynchronous testing is the best option available.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
0

My debounced function took arguments so I mocked _.debounce like this

spyOn(_, 'debounce').and.callFake(function(cb) {return cb});

(slight modification on @Wawy's answer)

NDavis
  • 1,127
  • 2
  • 14
  • 23