Unit Testing Angular Service that uses $timeout with Jasmine's Mock Clock
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();
});
});
Related videos on Youtube
![Jesus is Lord](https://i.stack.imgur.com/HP6W8.jpg?s=256&g=1)
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, 2022Comments
-
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 almost 11 yearsTry injecting
$rootScope
and calling$rootScope.$apply()
after pushing the clock forward.
-
-
Avi Cherry almost 9 yearsIsn'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 callexpect()
. -
Beez almost 9 yearsI'm not entirely sure. Might be worth a test, though.
-
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. Withdone
, you make sure that the test will not be completed untildone
is called. Anexpect
will not finish a test. You can have multiple expectations in a test. Also, if you provide thedone
callback but you never execute it, your test will fail because Jasmine will throw something likeexecution took too long
, because the test will never be completed. Can't remember the exact feedback. -
Beez almost 9 yearsAh, yes. That's right. So, pay attention to the
beforeEach
function which callsdone()
first. That's the key that tells Jasmine an async call is coming. So when$timeout.flush()
is called, it's pending until thedone()
is called after you're finishedexpect
-ing. Good note @Justus! -
ethanfar almost 9 yearsYour answer solves the problem, as well as reiterates an important rule of thumb: Unit tests should always be synchronous