Unit Testing Angular Service that uses $timeout with Jasmine's Mock Clock

37,868

Solution 1

Do not make your test Async by using Jasmine's clock. Instead, use $timeout.flush() to synchronously maintain the flow of the test. It may be a bit tricky to setup, but once you get it then your tests will be faster and more controlled.

Here's an example of a test that does it using this approach: https://github.com/angular/angular.js/blob/master/test/ngAnimate/animateSpec.js#L618

Solution 2

@matsko's answer led me down the right path. I thought I'd post my "complete" solution to make it simpler to find the answer.

The thing to test

angular.module("app").service("MyService", function() {
    return {
        methodThatHasTimeoutAndReturnsAPromise: function($q, $timeout) {
            var deferred = $q.defer();
            $timeout(function() {
                deferred.resolve(5);
            }, 2000);
            return deferred.promise;
        }
    };
});

The test

describe("MyService", function() {
    var target,
        $timeout;
    beforeEach(inject(function(_$timeout_, MyService) {
        $timeout = _$timeout_;
        target = MyService;
    }));
    beforeEach(function(done) {
        done();
    });
    it("equals 5", function(done) {
        target.methodThatHasTimeoutAndReturnsAPromise().then(function(value) {
            expect(value).toBe(5);
            done();
        });
        $timeout.flush();
    });
});
Share:
37,868

Related videos on Youtube

Jesus is Lord
Author by

Jesus is Lord

1 Corinthians 15:3-4 For I delivered to you first of all that which I also received: that Christ died for our sins according to the Scriptures, and that He was buried, and that He rose again the third day according to the Scriptures Romans 10:9 that if you confess with your mouth the Lord Jesus and believe in your heart that God has raised Him from the dead, you will be saved Philippians 2:10 that at the name of Jesus every knee should bow, of those in heaven, and of those on earth, and of those under the earth

Updated on July 23, 2022

Comments

  • Jesus is Lord
    Jesus is Lord almost 2 years

    I have a function inside one of my angular services that I'd like to be called repeatedly at a regular interval. I'd like to do this using $timeout. It looks something like this:

    var interval = 1000; // Or something
    
    var _tick = function () {
         $timeout(function () {
            doStuff();
            _tick();
        }, interval);
    };
    
    _tick();
    

    I'm stumped on how to unit test this with Jasmine at the moment - How do I do this? If I use $timeout.flush() then the function calls occur indefinitely. If I use Jasmine's mock clock, $timeout seems to be unaffected. Basically if I can get this working, I should be good to go:

    describe("ANGULAR Manually ticking the Jasmine Mock Clock", function() {
        var timerCallback, $timeout;
    
        beforeEach(inject(function($injector) {
            $timeout = $injector.get('$timeout');
            timerCallback = jasmine.createSpy('timerCallback');
            jasmine.Clock.useMock();
        }));
    
        it("causes a timeout to be called synchronously", function() {
            $timeout(function() {
                timerCallback();
            }, 100);
            expect(timerCallback).not.toHaveBeenCalled();
            jasmine.Clock.tick(101);
            expect(timerCallback).toHaveBeenCalled();
        });
    });
    

    These two variations work, but do not help me:

    describe("Manually ticking the Jasmine Mock Clock", function() {
        var timerCallback;
    
        beforeEach(function() {
            timerCallback = jasmine.createSpy('timerCallback');
            jasmine.Clock.useMock();
        });
    
        it("causes a timeout to be called synchronously", function() {
            setTimeout(function() {
                timerCallback();
            }, 100);
            expect(timerCallback).not.toHaveBeenCalled();
            jasmine.Clock.tick(101);
            expect(timerCallback).toHaveBeenCalled();
        });
    });
    
    describe("ANGULAR Manually flushing $timeout", function() {
        var timerCallback, $timeout;
    
        beforeEach(inject(function($injector) {
            $timeout = $injector.get('$timeout');
            timerCallback = jasmine.createSpy('timerCallback');
        }));
    
        it("causes a timeout to be called synchronously", function() {
            $timeout(function() {
                timerCallback();
            }, 100);
            expect(timerCallback).not.toHaveBeenCalled();
            $timeout.flush();
            expect(timerCallback).toHaveBeenCalled();
        });
    });
    

    Thanks in advance!

    • Hector Virgen
      Hector Virgen almost 11 years
      Try injecting $rootScope and calling $rootScope.$apply() after pushing the clock forward.
  • Avi Cherry
    Avi Cherry almost 9 years
    Isn't the done() in the "equals 5" test redundant? $timeout.flush() will synchronously call all of the pending events registered in $timeout, which will cause the promise to resolve, which will immediately call expect().
  • Beez
    Beez almost 9 years
    I'm not entirely sure. Might be worth a test, though.
  • Justus Romijn
    Justus Romijn almost 9 years
    @AviCherry Without the done callback parameter, the code will trigger the flush but then immediately consider the test passed, because it does not know that an asynchronous call was triggered. With done , you make sure that the test will not be completed until done is called. An expect will not finish a test. You can have multiple expectations in a test. Also, if you provide the done callback but you never execute it, your test will fail because Jasmine will throw something like execution took too long, because the test will never be completed. Can't remember the exact feedback.
  • Beez
    Beez almost 9 years
    Ah, yes. That's right. So, pay attention to the beforeEach function which calls done() first. That's the key that tells Jasmine an async call is coming. So when $timeout.flush() is called, it's pending until the done() is called after you're finished expect-ing. Good note @Justus!
  • ethanfar
    ethanfar almost 9 years
    Your answer solves the problem, as well as reiterates an important rule of thumb: Unit tests should always be synchronous