angular: testing: async test fails if SUT calls an observable with a delay operator (fakeAsync too)
I’m submitting a … (check one with “x”)
[x] bug report
[ ] feature request
[ ] support request
Consider these two tests:
afterEach(() => { expect(actuallyDone).toEqual(true); });
// Async
it('should run async test with successful delayed Observable', async(() => {
let actuallyDone = false;
let source = Observable.of(true).delay(10);
source.subscribe(
val => {
actuallyDone = true;
},
err => fail(err)
);
}));
// FakeAsync
it('should run async test with successful delayed Observable', fakeAsync(() => {
let source = Observable.of(true).delay(10);
source.subscribe(
val => {
actuallyDone = true;
},
err => fail(err)
);
tick();
}));
Current behavior
Test 1 fails with message: Cannot use setInterval from within an async zone test
Test 2 fails with message: Error: 1 periodic timer(s) still in the queue.
In neither test does the actuallyDone value become true;
Expected/desired behavior
The test should not fail.
Reproduction of the problem
See above.
What is the expected behavior?
The test should not fail. We should allow async tests of stuff that calls setInterval
. If we can’t, we had better offer a message that helps the developer find likely sources of the problem (e.g, Observables).
What is the motivation / use case for changing the behavior?
I have no idea how to test a SUT with an Observable.delay()
… or any observable operator that calls setInterval
.
Maybe I’m just using fakeAsync
incorrectly. I’d like to know what to do.
Let’s say we get that to work. Is that a solution?
I don’t think so. It is generally impractical for me, the test author, to anticipate whether fakeAsync
is necessary. I don’t always know if the SUT (or something it uses … like a service) makes use of setInterval
.
Moreover, a test that was working could suddenly fail simple because someone somewhere modified the observable with an operator that calls setInterval
. How would I know?
The message itself requires knowledge of which observables rely upon setInterval
. I only guessed that delay
did; it’s not obvious that it should.
Please tell us about your environment:
- Angular version: 2.0.0-rc.5 (candidate - 16 July 2016)
- Browser: [ Chrome ]
- Language: [TypeScript 1.8.x ]
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 49
- Comments: 52 (18 by maintainers)
After my investigation, the problem lies in how RxJS’ Scheduler works. This line on
delay.ts
, https://github.com/ReactiveX/rxjs/blob/master/src/operator/delay.ts#L82, it does a double check that it’s the time to dispatch the notification before actually dispatch the event by comparing the time to the Scheduler’snow
function.For
AsyncScheduler
, which is the default Scheduler fordelay()
operator, thenow
function is just a normal Date function. (See https://github.com/ReactiveX/rxjs/blob/master/src/Scheduler.ts#L26-L41) This is why the test pass when you set the breakpoint in @choeller’s test (https://github.com/angular/angular/issues/10127#issuecomment-242693488). Because Scheduler use the native Date.now function, before you continue running the code again, it have passed the 10 milliseconds already. Changing the delay to 1000 seconds and the test should not pass (unless you have enough patience to wait for 1000 seconds.)To make the fakeAsync test pass, one has to mock up the
Date.now
function, but usingjasmine.clock().mockDate(...)
might not going to work, because the Scheduler already have the nativeDate.now
set in thenow
property before Jasmine’s MockDate is installed, so you have to install MockDate before the Scheduler’snow
is set, which seems very impractical.Another workarounds I came up with is to spy on the
Scheduler.async.now
function to the mocked time, so the test should be like this:, which is a little hacky. You might also use something like
TestScheduler
orVirtualTimeScheduler
, which I have not used it before, so I don’t know if it’s going to work or not.I think the most viable solution is to let the fakeAsync’s
tick()
function mock the Date.now itself, which might be related to the issue #8678.I tried many workarounds but the only one I got to work was using
jasmine.done
instead ofasync
, which IIUC is what @wardbell suggested.@juliemr
discardPeriodicTasks()
prevents the “still in queue” error, but nevertheless the Observable is not executed - sotick
doesn’t seem to work with Observable.delay at all.we are currently running into this situation for Asynchroneous tests, that don’t complete even when adding multiple
tick
anddetectChanges
.@vikerman could you link to where the work is being done on this?
Would be good to have some guidance from angular team regarding how to handle this situation with unit tests.
as a workaround, I’m wrapping calls to .delay in my code with a check for the presence for jasmine. lame but effective at least for code we have control over
I’d like to work on a better long term solution, but as a quick note - you can get the fakeAsync() case working by calling
discardPeriodicTasks()
at the end.I also faced this issue and came up with solution of monkey-patching required time-based operators from RxJs which I’m using in the code under test. It works fine if you are not interested in testing time-wise but only functional-wise.
So for example if you are using debounceTime() operator, add:
And that will allow all your tests pass in sync way. That’s what I wanted to achieve and does not require any code changes but might feel a bit hacky.
From next version of
zone.js
, (0.8.21),fakeAsync
will supportjasmine.clock()
,Date.now
,rxjs.scheduler.delay
, https://github.com/angular/zone.js/pull/1009, so the test cases below will work without additional code, and I will add documentation afterzone.js
new version released.Date.now
andnew Date()
infakeAsync
.fakeAsync
test whenjasmine.clock().install
is called.Scheduler
support, need to importzone.js/dist/zone-patch-rxjs-fake-async
.We are coming up with a wrapper that makes it easier to use the TestBed in general and also specifically with rxjs.
I made a change to zone.js to allow setInterval in async tests. It’s up to you now to properly cancel the timer(or in the case of RxJS it will do it for you) - or your test will timeout.
https://github.com/angular/zone.js/pull/641
This will be available with the next release of zone.js.
Are there any plans on this? as far as I understand there is currently no way to test components that use setInterval (somewhere under the hood). This seems like a really serious limitation - or am I not getting something?
I successfully put @futurizing’s trick to work:
The “funny” thing is - when I start karma in a debug window and put a breakpoint on the tick-call the test works 😳
so there seem to be some weird things going on there… I’m using zone 0.6.15 and angular commit fc2fe00d16f4d685cbd6343afbca751125cbda54
@webcat12345 as a workaround, you could use
Observable.timer
:or manually
setTimeout
:Both should work in a
fakeAsync
zone withtick
.Does anyone tried the “flush()” at the end ? It worked for me
in
zone.js 0.8.25
, the following cases will work.I modified the case a little,
fakeAsync
need totick(10)
becausedelay(10)
. The case need to be runnable after https://github.com/angular/angular/pull/23108 is released. I also updated document here, https://github.com/angular/angular/pull/23117 and the test code, https://github.com/angular/angular/pull/23117/files#diff-b760eaece7529077c676429c3e28b43bR44@wardbell, @juliemr could you please review the PR https://github.com/angular/angular/pull/23117 about doc and test code change is ok or not? Thank you very much!
This is a pretty serious and surprising limitation! All my mocked http calls using angular-in-memory-web-api apparently uses setInterval behind the scenes, so I can not use Angular’s async to test any of them. If I try, the test fails with “Cannot use setInterval from within an async zone test.”.
Is feels like somewhat a joke that Angular2 docs really pushes RxJs but If one actually listen and do use RxJs, than the resulting code will not easily test (using any of the angular 2 supported methods).
I had to use plain old jasmine’s support for done callbacks to get tests with observables running. Quite a surprise. Much more complicated than I expected.
For now, I’m doing the following:
Then in my tests, I’m using
clock.tick(milliseconds)
.https://stackoverflow.com/questions/46881364/async-function-test-with-obseravable-delay-in-angular-unit-test-not-working
I am experiencing similar issue.
This is my mock Service and it is written with 10ms delay. And here goes my test case.
test does not wait for delay. Without delay it works. Is there anything wrong to my code? Delay on MockHttp is out of current zone?
I’m using zone.js 0.8.10 and angular 4.1.3. Don’t know if it really relates to this bug but can somebody point it out? After running this code I got “Error: 1 periodic timer(s) still in the queue.” always! Even discardPeriodicTasks doesn’t help
If I move .subscribe to it than it magically works
@awerlang What version do you need for this to run? with angular 2.4.8, zone 0.7.7, rxjs 5.2.0 I get
It seems to be fixed now for
fakeAsync()
contexts:EDIT(2017-02-23): I was mistaken,
delay
still doesn’t work,debounceTime
does work.For
async()
we still can’t have interval-based timer.@IliaVolk Life saver, thanks!
You can turn it into a custom operator for convenience:
You can replace
delay(10)
withswitchMap(value => timer(10).pipe(mapTo(value)))
.Any update on this issue? Is it fixed in some combination of versions?
@leonadler Thanks for the code snippet, this helped me a lot,
Just a quick note in case this helps anyone else, I found that in order to get your workaround to work I also had to specify as an argument to the tick() call in fakeAsync(), a value in milliseconds at least equal to the delay value specified in my Observable.timer(). e.g. tick(10) for Observable.timer(10).
The problem is that rxjs has its own time passing mechanism. Your code would need to inject the rxjs default scheduler and use it on every pipe operation, you could then change it on tests with rxjs TestScheduler.
You could also “hack” into the default scheduler (I believe thats
AsyncScheduler
) and control time yourself. I know for sure that that works, but it may be tedious to set up. Someone wrote a Medium post about it in extensive detail, and shows different options. I personally like theirAsyncZoneTimeInSyncKeeper
approach.@Fujivato yes,
tick()
without atime
value is just for immediates (“run now” timeouts).Microtasks attached to a specific time are only ran after that time is
tick
ed, as you would expect.The same is valid for periodic timers, of course
I had a similar problem with Observable.delay() and jasmine.clock().tick() and found a workaround by replacing
Observable.of(true).delay(10)
withObservable.timer(10).map(() => true)
Maybe avoiding Observable.delay() this way is a workaround for this problem as well.
@Necroskillz Yeah, it doesn’t seem to work for me either.
The problem seems to be here: https://github.com/ReactiveX/rxjs/blob/9c9870e5da3e2f55bbd57be25f6d164565972d49/src/operator/delay.ts#L82
The test passes if I’m debugging (since it takes more than 10 milliseconds to step through to that line).
scheduler.now()
returns the current time, I guess the angular code needs to provide its own scheduler to override thisnow()
and only increment it on calls totick()
? Or maybe instead of creating a new scheduler, usingVirtualTimeScheduler.advanceBy(time)