-
Notifications
You must be signed in to change notification settings - Fork 25.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
testing: async test fails if SUT calls an observable with a delay operator (fakeAsync too) #10127
Comments
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 |
@juliemr it('should be able to work with Observable.delay', fakeAsync(() => {
let actuallyDone=false;
let source = Observable.of(true).delay(10);
source.subscribe(
val => {
actuallyDone = true;
},
err => fail(err)
);
tick(100);
expect(actuallyDone).toBeTruthy(); // Expected false to be truthy.
discardPeriodicTasks();
})); we are currently running into this situation for Asynchroneous tests, that don't complete even when adding multiple |
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? |
The "funny" thing is - when I start karma in a debug window and put a breakpoint on the tick-call the test works 😳 it('should be able to work with Observable.delay', fakeAsync(() => {
let actuallyDone=false;
let source = Observable.of(true).delay(10);
source.subscribe(
val => {
actuallyDone = true;
},
err => fail(err)
);
tick(100); // ---------------> put breakpoint here in dev-console
expect(actuallyDone).toBeTruthy(); // succeedes
discardPeriodicTasks();
})); so there seem to be some weird things going on there... I'm using zone 0.6.15 and angular commit fc2fe00 |
I have the same issue with a different use case. An Angular 2 component that wraps some D3 magic which requires The behavior is exactly as described in the original bug report and I agree with @choeller that this is a serious limitation. The only thing I noticed is, that as long as I do not run
|
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
|
@choeller One thing to keep in mind ... we can fall back to |
@wardbell I generally prefer jasmine.done over async and fakeAsync but as far as I understand we can only utilize it, when the async part is happening in the test itself (like with |
Haven't tried yet but with Jasmine clock and spies might plow through it more easily. Will try again when I'm back |
After my investigation, the problem lies in how RxJS' Scheduler works. For To make the fakeAsync test pass, one has to mock up the Another workarounds I came up with is to spy on the it('should run async test with successful delayed Observable', fakeAsync(() => {
let actuallyDone = false;
let currentTime = 0;
spyOn(Scheduler.async, 'now').and.callFake(() => currentTime);
let source = Observable.of(true).delay(10);
source.subscribe(() => {
actuallyDone = true;
});
currentTime = 10;
tick(10);
expect(actuallyDone).toEqual(true);
})); , which is a little hacky. You might also use something like I think the most viable solution is to let the fakeAsync's |
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. |
I can confirm this is still a bug in angular 2.1.1. Pretty annoying... |
Would be good to have some guidance from angular team regarding how to handle this situation with unit tests. |
Are there any news on this, also anyone got a workaround tried @futurizing solution but no luck |
I tried many workarounds but the only one I got to work was using // Instead of having this:
it('...', async(() => {
fixture.whenStable().then(() => {
// Your test here.
});
});
// I had to do this:
it('...', (done) => {
fixture.whenStable().then(() => {
// Your test here.
done();
});
}); |
Well, just came to the same thing and the solution I found for myself is to use a from-Promise way which is working magically: resourceReadSpy = spyOn(resource, 'read').and.returnValue({
$observable: Observable.from(Promise.resolve(true))
}); This just works inside of This of course does not solve the problem with intervals etc. but this is how at least the HTTP calls could be mocked... |
Someone correct me if I'm wrong here, but aside from the Angular team being a big proponent of the RxJs library, and despite its integration with Jasmine, the real root of this issue is actually a problem between RxJs and Jasmine itself, correct? |
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. So for example if you are using debounceTime() operator, add: beforeAll(() => {
// Monkey-patch Observable.debounceTime() since it is using
// setInterval() internally which not allowed within async zone
Observable.prototype.debounceTime = function () { return this; };
}); And that will allow all your tests pass in sync way. |
It seems to be fixed now for
EDIT(2017-02-23): I was mistaken, For |
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. This will be available with the next release of zone.js. |
@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
|
@Necroskillz Yeah, it doesn't seem to work for me either. The problem seems to be here: The test passes if I'm debugging (since it takes more than 10 milliseconds to step through to that line). |
@Necroskillz I was mistaken, I was using |
I successfully put @futurizing's trick to work:
|
I one more trick that worked for me. I'm using function observableDelay(value: any, delayMs: number) {
return Observable.interval(delayMs).take(1).map(() => value);
} |
@vikerman Great! Can you give any estimation when this fix will be released? (zone 0.7.8?) |
The fix should be in the latest release of Zone.js a d Angular has been updated to work with it. |
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?
If I move .subscribe to it than it magically works |
I had a similar problem with Observable.delay() and jasmine.clock().tick() and found a workaround by replacing Maybe avoiding Observable.delay() this way is a workaround for this problem as well. |
I'd like to point to another workaround: https://stackoverflow.com/a/46027549/395990 The idea here is to expose
Check out the other answers on the thread for alternatives, too. |
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? |
@webcat12345 as a workaround, you could use post(url, body, option): Observable<Response> {
let resOpt = new ResponseOptions({
body: JSON.stringify({success: true})
});
let res: Response = new Response(resOpt);
// Workaround for https://github.com/angular/angular/issues/10127
return Observable.timer(10, Number.Infinity).take(1).mapTo(res);
} or manually post(url, body, option): Observable<Response> {
let resOpt = new ResponseOptions({
body: JSON.stringify({success: true})
});
let res: Response = new Response(resOpt);
// Workaround for https://github.com/angular/angular/issues/10127
return new Observable(subscriber => {
setTimeout(() => {
subscriber.next(res);
subscriber.complete();
}, 10);
});
} Both should work in a |
@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). |
@Fujivato yes,
Microtasks attached to a specific time are only ran after that time is
The same is valid for periodic timers, of course
|
Any update on this issue? Is it fixed in some combination of versions? |
We are coming up with a wrapper that makes it easier to use the TestBed in general and also specifically with rxjs. |
@vikerman could you link to where the work is being done on this? |
For now, I'm doing the following: describe('thing', () => {
let clock: jasmine.Clock;
beforeEach(() => {
clock = jasmine.clock();
clock.mockDate();
clock.install();
});
afterEach(() => {
clock.uninstall();
});
}); Then in my tests, I'm using |
From next version of
fakeAsyncTestZone.run(() => {
const start = Date.now();
testZoneSpec.tick(100);
const end = Date.now();
expect(end - start).toBe(100);
});
fakeAsyncTestZone.run(() => {
const start = new Date();
testZoneSpec.tick(100);
const end = new Date();
expect(end.getTime() - start.getTime()).toBe(100);
});
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should get date diff correctly', () => { // we don't need fakeAsync here.
// automatically run into fake async zone, because jasmine.clock() is installed.
const start = Date.now();
jasmine.clock().tick(100);
const end = Date.now();
expect(end - start).toBe(100);
});
import '../../lib/rxjs/rxjs-fake-async';
it('should get date diff correctly', (done) => {
fakeAsyncTestZone.run(() => {
let result = null;
const observable = new Observable((subscribe: any) => {
subscribe.next('hello');
});
observable.delay(1000).subscribe(v => {
result = v;
});
expect(result).toBeNull();
testZoneSpec.tick(1000);
expect(result).toBe('hello');
done();
});
}); |
@bryanforbes do you mean it works in Observable.delay situation? |
in 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(10); // here need to tick 10
})); I modified the case a little, @wardbell, @juliemr |
I am having a problem with milliseconds. My zone.js
My version of the code:
It worked, but the 5000 milliseconds timings are not what I expected:
|
You can replace |
@PHuhn , you can try |
@IliaVolk Life saver, thanks! You can turn it into a custom operator for convenience: function delay(delayMs) {
return source => source.pipe(switchMap(value => timer(delayMs).pipe(mapTo(value))));
}
of('foo').pipe(delay(10)); |
Does anyone tried the "flush()" at the end ? It worked for me |
I am resolving the
|
Wondering what's the status of this issue?
The one working is replacing |
I have tried with fakeAsync and flush/tick. Nothing worked for me. As @Goodwine suggested, I have changed to use Jasmin I still get the error Here is my integration testing code
|
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 |
This is solved by @JiaLiPassion. Just do the setup as described in angular/zone.js#1009 (comment) if using Angular 5. |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
I'm submitting a ... (check one with "x")
Consider these two tests:
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 callssetInterval
.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 ofsetInterval
.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 thatdelay
did; it's not obvious that it should.Please tell us about your environment:
The text was updated successfully, but these errors were encountered: