jasmine.clock().tick() does not work with $timeout and debounce, but works fine with setTimeout

All we need is an easy explanation of the problem, so here it is.

Below I have 3 functions that do exactly the same thing. Each one uses a different way of calling setTimeout, delay1() uses setTimeout directly, delay2() uses angularjs $timeout and delay3() uses lodash debounce. They all work fine.

The problems occurs when I test using Jasmine. setTimeout works fine with the jasmine.clock().tick() method, but $timeout and debounce don’t

I am interested in getting debounce working with Jasmine. I know I can use $timeout.flush() with angularjs but $timeout and setTimeout give me problems elsewhere in my code where I am using it with leaflet maps. debounce works nicely with leaflet.

I have created a plunker here: plnkr where you will see the $timeout and debounce tests not passing while the setTimeout test passes.

Is there a way I can work around this problem? Thanks

JS

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope, $timeout) {
  $scope.name = 'World';
  $scope.delayed1 = function(){
    setTimeout(function(){
      $scope.name = "Hello world by setTimeout";
    },500)
  }
  $scope.delayed2 = function(){
    $timeout(function(){
      $scope.name = "Hello world by $timeout";
    },500)
  }
  $scope.delayed3 = function(){
    _.debounce(function(){
      $scope.name = "Hello world by debounce";
    },500)
  }
});

spec

describe('Testing a Hello World controller', function() {
  var $scope = null;
  var ctrl = null;

  //you need to indicate your module in a test
  beforeEach(module('plunker'));

  beforeEach(inject(function($rootScope, $controller) {
    $scope = $rootScope.$new();

    ctrl = $controller('MainCtrl', {
      $scope: $scope
    });
  }));

  it('should say hallo to the World', function() {
    expect($scope.name).toEqual('World');
  });
  it('should say Hello world by setTimeout', function() {
    jasmine.clock().install();
    $scope.delayed1();
    jasmine.clock().tick(600);
    expect($scope.name).toEqual('Hello world by setTimeout');
    jasmine.clock().uninstall();

  });
  it('should say Hello world by timeout', function() {
    jasmine.clock().install();
    $scope.delayed2();
    jasmine.clock().tick(600);
    expect($scope.name).toEqual('Hello world by timeout');
    jasmine.clock().uninstall();

  }); 
  it('should say Hello world by debouce', function() {
    jasmine.clock().install();
    $scope.delayed3();
    jasmine.clock().tick(600);
    expect($scope.name).toEqual('Hello world by debouce');
    jasmine.clock().uninstall();

  }); 
});

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

The clock in Jasmine will only work if you’re testing setInterval() or setTimeout() functions directly as it just mocks those functions specifically to run synchronously. I believe there’s a pull request for Jasmine to mock the Date object, which would allow for testing functions like _.debounce() without mocking it, but I don’t remember if that was merged in or not.

To test _.debounce() you’ll have to mock it to run synchronously, preferably as a spy or you can just override the function. Here’s what I’ve found to work for me:

spyOn(_, 'debounce').and.callFake(function (func) {
    return function () {
        func.apply(this, arguments);
    };
});

Now calls to _.debounce() will run synchronously and tests should complete successfully. Of course you’ll still have to use $timeout.flush().

I updated your plunker with these changes: http://plnkr.co/edit/KXmwcf1faUNf8nlqPeyd

Method 2

The debounce function from lodash uses the Date object. You mock the Date object using jasmine like this:

jasmine.clock().install();
jasmine.clock().mockDate();

jasmine.clock().tick(1000); // trigger the debounce

Source: https://jasmine.github.io/2.9/introduction.html#section-Mocking_the_Date

Method 3

In addition to @JDWardle’s answer you maye also want to create a spy for the debounce cancel method.

spyOn(_, 'debounce').and.callFake(function (func) {
    var mockDebounce = function () {
        func.apply(this, arguments);
    };
    mockDebounce.cancel = jasmine.createSpy('cancel');
    return mockDebounce;
});

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply