GC_
GC_

Reputation: 508

How to unit test / mock a $timeout call?

How do I mock the timeout call, here?

$scope.submitRequest = function () {

    var formData = getData();

    $scope.form = JSON.parse(formData);

    $timeout(function () {
        $('#submitForm').click();            
    }, 2000);

};

I want to see timeout has been called with the correct function.

I would like an example of the spyon function mocking $timeout.

spyOn(someObject,'$timeout')

Upvotes: 4

Views: 2455

Answers (6)

Zohaib Jawaid
Zohaib Jawaid

Reputation: 143

I totally agree with Frane Poljak's answer. You should surely follow his way. Second way to do it is by mocking $timeout service like below:

describe('MainController', function() {
var $scope, $timeout;

beforeEach(module('app'));

beforeEach(inject(function($rootScope, $controller, $injector) {
  $scope = $rootScope.$new();
  $timeout = jasmine.createSpy('$timeout');
  $controller('MainController', {
    $scope: $scope,
    $timeout: $timeout
  });
}));

it('should submit request', function() {
  $scope.submitRequest();
  expect($timeout).toHaveBeenCalled();
});

Here is the plunker having both approaches: http://plnkr.co/edit/s5ls11

Upvotes: 1

Estus Flask
Estus Flask

Reputation: 222528

$timeout can be spied or mocked as shown in this answer:

beforeEach(module('app', ($provide) => {
  $provide.decorator('$timeout', ($delegate) => {
    var timeoutSpy = jasmine.createSpy().and.returnValue($delegate);
    // methods aren't copied automatically to spy
    return angular.extend(timeoutSpy, $delegate);
  });
}));

There's not much to test here, since $timeout is called with anonymous function. For testability reasons it makes sense to expose it as scope/controller method:

$scope.submitFormHandler = function () {
    $('#submitForm').click();            
};

...
$timeout($scope.submitFormHandler, 2000);

Then spied $timeout can be tested:

$timeout.and.stub(); // in case we want to test submitFormHandler separately
scope.submitRequest();
expect($timeout).toHaveBeenCalledWith(scope.submitFormHandler, 2000);

And the logic inside $scope.submitFormHandler can be tested in different test.

Another problem here is that jQuery doesn't work well with unit tests and requires to be tested against real DOM (this is one of many reasons why jQuery should be avoided in AngularJS applications when possible). It's possible to spy/mock jQuery API like shown in this answer.

$(...) call can be spied with:

var init = jQuery.prototype.init.bind(jQuery.prototype);
spyOn(jQuery.prototype, 'init').and.callFake(init);

And can be mocked with:

var clickSpy = jasmine.createSpy('click');
spyOn(jQuery.prototype, 'init').and.returnValue({ click: clickSpy });

Notice that it's expected that mocked function will return jQuery object for chaining with click method.

When $(...) is mocked, the test doesn't require #submitForm fixture to be created in DOM, this is the preferred way for isolated unit test.

Upvotes: 4

Manoj Bhardwaj
Manoj Bhardwaj

Reputation: 858

Unit Tesitng $timeout with flush delay

You have to flush the queue of the $timeout service by calling $timeout.flush()

describe('controller: myController', function(){
describe('showAlert', function(){
    beforeEach(function(){
        // Arrange
        vm.alertVisible = false;

        // Act
        vm.showAlert('test alert message');
    });

    it('should show the alert', function(){
        // Assert
        assert.isTrue(vm.alertVisible);
    });

    it('should hide the alert after 5 seconds', function(){
        // Act - flush $timeout queue to fire off deferred function
        $timeout.flush();

        // Assert
        assert.isFalse(vm.alertVisible);
    });
  })
});

Please checkout this link http://jasonwatmore.com/post/2015/03/06/angularjs-unit-testing-code-that-uses-timeout

Upvotes: 1

PM1
PM1

Reputation: 161

Assuming that piece of code is within the controller or being created in the test by $controller, then $timeout can be passed in the construction parameter. So you could just do something like:

var timeoutStub = sinon.stub();
var myController = $controller('controllerName', timeoutStub);
$scope.submitRequest();
expect(timeoutStub).to.have.been.called;

Upvotes: 1

Petr Averyanov
Petr Averyanov

Reputation: 9476

Create mock for $timeout provider:

var f = () => {} 
var myTimeoutProviderMock = () => f;

Use it:

beforeEach(angular.mock.module('myModule', ($provide) => {
  $provide.factory('$timeout', myTimeoutProviderMock);
}))

Now you can test:

spyOn(f);
expect(f).toHaveBeenCalled();

P.S. you'd better test result of function in timeout.

Upvotes: 1

Frane Poljak
Frane Poljak

Reputation: 2365

First of all, DOM manipulation should only be performed in directives. Also, it's better to use angular.element(...), than $(...). Finally, to do this, you can expose your element's click handler to the scope, spy on it, and check if that handler has been called:

$timeout.flush(2000);
$timeout.verifyNoPendingTasks();
expect(scope.myClickHandler).toHaveBeenCalled();

EDIT:

since that's a form and there is no ng-click handler, you can use ng-submit handler, or add a name to your form and do:

$timeout.flush(2000);
$timeout.verifyNoPendingTasks();
expect(scope.formName.$submitted).toBeTruthy();

Upvotes: 5

Related Questions