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)

Most upvoted comments

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’s now function.

For AsyncScheduler, which is the default Scheduler for delay() operator, the now 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 using jasmine.clock().mockDate(...) might not going to work, because the Scheduler already have the native Date.now set in the now property before Jasmine’s MockDate is installed, so you have to install MockDate before the Scheduler’s now 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:

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 TestScheduler or VirtualTimeScheduler, 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 of async, which IIUC is what @wardbell suggested.

// 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();
  });
});

@juliemr discardPeriodicTasks() prevents the “still in queue” error, but nevertheless the Observable is not executed - so tick doesn’t seem to work with Observable.delay at all.

  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 tick and detectChanges.

@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

    if(!window.jasmine) {
        myObservable = myObservable.delay(500);
    }

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:

  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. 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 support jasmine.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 after zone.js new version released.

  • support auto patch Date.now and new Date() in fakeAsync.
 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);
  });
  • automatically run a fakeAsync test when jasmine.clock().install is called.
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);
    });
  • rxjs Scheduler support, need to import zone.js/dist/zone-patch-rxjs-fake-async.
    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();
      });
    });

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:

import { tick as _tick, discardPeriodicTasks } from '@angular/core/testing';
import { async as _async } from 'rxjs/scheduler/async';

function getTick() {
    let currentTime = 0;
    spyOn(_async, 'now').and.callFake(() => currentTime);

    return delay => {
        currentTime = delay;
        _tick(delay);
    };
}
...
const tick = getTick();
tick(2000);

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 fc2fe00d16f4d685cbd6343afbca751125cbda54

@webcat12345 as a workaround, you could use Observable.timer:

  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 setTimeout:

  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 fakeAsync zone with tick.

Does anyone tried the “flush()” at the end ? It worked for me

in zone.js 0.8.25, the following cases will work.

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, fakeAsync need to tick(10) because delay(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:

describe('thing', () => {
    let clock: jasmine.Clock;

    beforeEach(() => {
        clock = jasmine.clock();
        clock.mockDate();
        clock.install();
    });

    afterEach(() => {
        clock.uninstall();
    });
});

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.

class MockHttp {   
  post(url, body, option): Observable<Response> {
    let resOpt = new ResponseOptions({
      body: JSON.stringify({success: true})
    });
    let res: Response = new Response(resOpt);
    return Observable.of(res).delay(10);
  }
}

This is my mock Service and it is written with 10ms delay. And here goes my test case.

it('http post should get valid response without any param', fakeAsync(() => {
    let retVal = null;
    // note this is just blackbox testing. httpHelperSerivce.post() function is wrapper of http.post(p1, p2, p3), 
    httpHelperService.post('url', {a: 'a'}).subscribe(res => {
      console.log(res);
      retVal = res;
    });
    tick();
    expect(retVal).toEqual({success: true});
    discardPeriodicTasks();
  }));

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

describe(`test observable fakeasync`, () => {
    let subscription : Subscription;

    beforeEach(fakeAsync(() => {
        subscription = Observable.interval(1000).subscribe(() => {});
    }));

    it (`should be ok`, fakeAsync(() => {
        subscription.unsubscribe();
        // Even this doesn't help
        // discardPeriodicTasks();
    }))

})

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

Expected undefined to be true.

Error: 1 periodic timer(s) still in the queue.

It seems to be fixed now for fakeAsync() contexts:

// FakeAsync
it('should run async test with successful delayed Observable', fakeAsync(() => {
  let actuallyDone;
  let source = Observable.of(true).delay(10);
  source.subscribe(
    val => {
      actuallyDone = true;
    },
    err => fail(err)
  );
  tick(10);
  expect(actuallyDone).toBe(true);
}));

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:

function delay(delayMs) {
    return source => source.pipe(switchMap(value => timer(delayMs).pipe(mapTo(value))));
}

of('foo').pipe(delay(10));

You can replace delay(10) with switchMap(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 their AsyncZoneTimeInSyncKeeper approach.

@Fujivato yes, tick() without a time value is just for immediates (“run now” timeouts).

let resolved = false;
Promise.resolve().then(() => resolved = true);
let timeoutRan = false;
setTimeout(() => timeoutRan = true);

expect(resolved).toBe(false);
expect(timeoutRan).toBe(false);
tick();
expect(resolved).toBe(true);
expect(timeoutRan).toBe(true);

Microtasks attached to a specific time are only ran after that time is ticked, as you would expect.

let oneSecondPassed = false;
setTimeout(() => oneSecondPassed = true, 1000);

expect(oneSecondPassed).toBe(false);
tick(400);
expect(oneSecondPassed).toBe(false);
tick(500);
expect(oneSecondPassed).toBe(false);
tick(100);
expect(oneSecondPassed).toBe(true);

The same is valid for periodic timers, of course

let secondsPassed = 0;
setInterval(() => secondsPassed++, 1000);

expect(secondsPassed).toBe(0);
tick(900);
expect(secondsPassed).toBe(0);
tick(100);
expect(secondsPassed).toBe(1);
tick(2000);
expect(secondsPassed).toBe(3);

I had a similar problem with Observable.delay() and jasmine.clock().tick() and found a workaround by replacing Observable.of(true).delay(10) with Observable.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 this now() and only increment it on calls to tick() ? Or maybe instead of creating a new scheduler, using VirtualTimeScheduler.advanceBy(time)