Solvedangular 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 ]
53 Answers

✔️Accepted Answer

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 (#10127 (comment)). 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.

Other Answers:

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.

Would be good to have some guidance from angular team regarding how to handle this situation with unit tests.

@vikerman could you link to where the work is being done on this?

More Issues: