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

Closed
wardbell opened this Issue Jul 18, 2016 · 51 comments

Comments

Projects
None yet
@wardbell
Copy link
Contributor

wardbell commented Jul 18, 2016

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 ]
@juliemr

This comment has been minimized.

Copy link
Member

juliemr commented Jul 20, 2016

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.

@choeller

This comment has been minimized.

Copy link
Contributor

choeller commented Aug 17, 2016

@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.

@choeller

This comment has been minimized.

Copy link
Contributor

choeller commented Aug 25, 2016

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?

@choeller

This comment has been minimized.

Copy link
Contributor

choeller commented Aug 26, 2016

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

@tomwanzek

This comment has been minimized.

Copy link

tomwanzek commented Sep 7, 2016

I have the same issue with a different use case.

An Angular 2 component that wraps some D3 magic which requires setInterval.

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 fixture.detectChanges(), I can at least validate the static parts of the component template and e.g. component member initialization done in the constructor. The setInterval is not invoked until the ngOnInit lifecyclehook is hit.

import { TestBed, async } from '@angular/core/testing';
import { TestVoronoiSpirals3Component } from './test-voronoi-spirals-3.component';

import { D3Service, D3 } from 'd3-ng2-service';

describe('Component: TestVoronoiSpirals3', () => {

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [
        TestVoronoiSpirals3Component
      ],
      providers: [
        D3Service
      ]
    });
  });

  it('should create the component', async(() => {
    let fixture = TestBed.createComponent(TestVoronoiSpirals3Component);
    let component = fixture.debugElement.componentInstance;
    expect(component).toBeTruthy();
  }));

  it('should have a d3 member with  relevant D3 members', async(() => {
    let fixture = TestBed.createComponent(TestVoronoiSpirals3Component);
    let component = fixture.debugElement.componentInstance;
    let d3: D3 | undefined = component.d3;
    expect(d3).toBeTruthy('No member d3 defined');
    expect(d3 && d3.voronoi && typeof d3.voronoi === 'function').toBeTruthy('Member function voronoi() of d3 not defined');
  }));

  it(`should have heading h2 with text 'Voronoi Spirals III Example'`, async(() => {
    let fixture = TestBed.createComponent(TestVoronoiSpirals3Component);
    let compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h2').textContent).toBe('Voronoi Spirals III Example');
  }));

  it('should have exactly one HTMLCanvasElement with dimensions 400x400', async(() => {
    let fixture = TestBed.createComponent(TestVoronoiSpirals3Component);
    let compiled = fixture.debugElement.nativeElement;
    let canvasElements = <NodeListOf<HTMLCanvasElement>>compiled.querySelectorAll('canvas');
    expect(canvasElements.length).toBe(1, 'Incorrect number of canvas elements found');
    expect(canvasElements.length === 1 && canvasElements[0].clientWidth).toBe(400, 'Incorrect canvas width');
    expect(canvasElements.length === 1 && canvasElements[0].clientHeight).toBe(400, 'Incorrect canvas height');
  }));


});
@robertlevy

This comment has been minimized.

Copy link

robertlevy commented Sep 7, 2016

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);
    }
@wardbell

This comment has been minimized.

Copy link
Contributor Author

wardbell commented Sep 9, 2016

@choeller One thing to keep in mind ... we can fall back to jasmine.done. I intend to do that on my sample and see if I can get the job done that way.

@choeller

This comment has been minimized.

Copy link
Contributor

choeller commented Sep 10, 2016

@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 whenStable). I "solved" our use case (testing a component with multiple debounceTime calls) by introducing a complex cascade of setTimeouts in the test and calling done in the deepest cascade, but that is really super hacky...

@wardbell

This comment has been minimized.

Copy link
Contributor Author

wardbell commented Sep 11, 2016

Haven't tried yet but with Jasmine clock and spies might plow through it more easily. Will try again when I'm back

@kasiditi

This comment has been minimized.

Copy link
Contributor

kasiditi commented Sep 18, 2016

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.

@mmc41

This comment has been minimized.

Copy link

mmc41 commented Oct 6, 2016

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.

@JohannesRudolph

This comment has been minimized.

Copy link

JohannesRudolph commented Oct 27, 2016

I can confirm this is still a bug in angular 2.1.1. Pretty annoying...

@girishjjain

This comment has been minimized.

Copy link

girishjjain commented Oct 27, 2016

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

@FabioAntunes

This comment has been minimized.

Copy link

FabioAntunes commented Nov 4, 2016

Are there any news on this, also anyone got a workaround tried @futurizing solution but no luck

@Goodwine

This comment has been minimized.

Copy link
Contributor

Goodwine commented Nov 6, 2016

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

This comment has been minimized.

Copy link

smnbbrv commented Nov 10, 2016

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 async because it does not use Scheduler (I assume) and the native Promise implementation does not use any of setTimeout / setInterval.

This of course does not solve the problem with intervals etc. but this is how at least the HTTP calls could be mocked...

@theBull

This comment has been minimized.

Copy link

theBull commented Nov 11, 2016

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?

@gund

This comment has been minimized.

Copy link

gund commented Nov 22, 2016

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.

@awerlang

This comment has been minimized.

Copy link
Contributor

awerlang commented Jan 27, 2017

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.

@vikerman

This comment has been minimized.

Copy link
Contributor

vikerman commented Feb 15, 2017

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.

angular/zone.js#641

This will be available with the next release of zone.js.

@Necroskillz

This comment has been minimized.

Copy link

Necroskillz commented Feb 23, 2017

@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.
@kevindqc

This comment has been minimized.

Copy link

kevindqc commented Feb 23, 2017

@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)

@awerlang

This comment has been minimized.

Copy link
Contributor

awerlang commented Feb 24, 2017

@Necroskillz I was mistaken, I was using debounceTime() instead. Today I wanted to test a delay() and it didn't worked. Sorry for that 😞

@awerlang

This comment has been minimized.

Copy link
Contributor

awerlang commented Feb 24, 2017

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);
@DethAriel

This comment has been minimized.

Copy link

DethAriel commented Feb 24, 2017

I one more trick that worked for me. I'm using fakeAsync tests, rxjs 5.0.2, angular 2.4.7, zone.js 0.7.4:

function observableDelay(value: any, delayMs: number) {
  return Observable.interval(delayMs).take(1).map(() => value);
}
@choeller

This comment has been minimized.

Copy link
Contributor

choeller commented Mar 7, 2017

@vikerman Great! Can you give any estimation when this fix will be released? (zone 0.7.8?)

@vikerman

This comment has been minimized.

Copy link
Contributor

vikerman commented Mar 19, 2017

The fix should be in the latest release of Zone.js a d Angular has been updated to work with it.

@sherlock1982

This comment has been minimized.

Copy link

sherlock1982 commented May 31, 2017

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

@Nivani

This comment has been minimized.

Copy link

Nivani commented Jun 7, 2017

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.

@vivri

This comment has been minimized.

Copy link

vivri commented Oct 6, 2017

I'd like to point to another workaround: https://stackoverflow.com/a/46027549/395990

The idea here is to expose done() alongside inject([ ... ], ...):

it('should work', (done) => inject([SomeService], (someService: SomeService) =>
{
  const t = true;
  setTimeout (() => {
    expect(t).toEqual(true);
    done();
  }, 1000);
})());

Check out the other answers on the thread for alternatives, too.

@webcat12345

This comment has been minimized.

Copy link

webcat12345 commented Oct 23, 2017

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?

@leonadler

This comment has been minimized.

Copy link

leonadler commented Nov 2, 2017

@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.

@Fujivato

This comment has been minimized.

Copy link

Fujivato commented Nov 23, 2017

@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).

@leonadler

This comment has been minimized.

Copy link

leonadler commented Nov 24, 2017

@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);
@Bbbrinks

This comment has been minimized.

Copy link

Bbbrinks commented Dec 20, 2017

Any update on this issue? Is it fixed in some combination of versions?

@vikerman

This comment has been minimized.

Copy link
Contributor

vikerman commented Jan 26, 2018

We are coming up with a wrapper that makes it easier to use the TestBed in general and also specifically with rxjs.

@vikerman vikerman closed this Jan 26, 2018

@JSMike

This comment has been minimized.

Copy link

JSMike commented Jan 28, 2018

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

@bryanforbes

This comment has been minimized.

Copy link

bryanforbes commented Feb 15, 2018

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).

@JiaLiPassion

This comment has been minimized.

Copy link
Contributor

JiaLiPassion commented Feb 16, 2018

From next version of zone.js, (0.8.21), fakeAsync will support jasmine.clock(), Date.now, rxjs.scheduler.delay, angular/zone.js#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();
      });
    });
@deli6z

This comment has been minimized.

Copy link

deli6z commented Mar 1, 2018

@bryanforbes do you mean it works in Observable.delay situation?
Still no luck for me, tried every possible way described earlier here.

@JiaLiPassion

This comment has been minimized.

Copy link
Contributor

JiaLiPassion commented Apr 4, 2018

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 #23108 is released.
I also updated document here, #23117
and the test code, https://github.com/angular/angular/pull/23117/files#diff-b760eaece7529077c676429c3e28b43bR44

@wardbell, @juliemr
could you please review the PR #23117 about doc and test code change is ok or not? Thank you very much!

@PHuhn

This comment has been minimized.

Copy link

PHuhn commented Apr 11, 2018

I am having a problem with milliseconds. My zone.js

"rxjs": "^5.5.9",
"webpack": "^3.11.0",
"zone.js": "^0.8.26"

My version of the code:

// FakeAsync
it('should run async test with successful delayed Observable', fakeAsync(() => {
	let actuallyDone = false;
	let source = Observable.of( true ).delay( 10 );
	source.subscribe(
		val => {
			actuallyDone = true;
			console.log( `** Actually-done ${new Date().toISOString()}` )
		},
		err => {
			console.log( '******* Actually-done fail *******' );
			fail(err);
		}
	);
	console.log( `** ${new Date().toISOString()}` )
	tick( 5000 ); // here need to tick 10
	console.log( `** ${new Date().toISOString()}` )
	expect( actuallyDone ).toEqual( true );
}));

It worked, but the 5000 milliseconds timings are not what I expected:

** 2018-04-11T22:50:35.339Z
** Actually-done 2018-04-11T22:50:35.349Z
** 2018-04-11T22:50:35.350Z

@IliaVolk

This comment has been minimized.

Copy link

IliaVolk commented May 8, 2018

You can replace delay(10) with switchMap(value => timer(10).pipe(mapTo(value))).

@JiaLiPassion

This comment has been minimized.

Copy link
Contributor

JiaLiPassion commented May 8, 2018

@PHuhn , you can try import '../../lib/rxjs/rxjs-fake-async'; in your test.ts.

@benelliott

This comment has been minimized.

Copy link
Contributor

benelliott commented May 18, 2018

@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));
@rvmladenov

This comment has been minimized.

Copy link

rvmladenov commented Jun 6, 2018

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

@akolybelnikov

This comment has been minimized.

Copy link

akolybelnikov commented Jul 11, 2018

I am resolving the .compileComponents promise, set a tick and it works like so:

beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      // your imports
    })
      .compileComponents().then(() => {
        tick(200)
      })

    fixture = TestBed.createComponent(Component);
    component = fixture.componentInstance;
    fixture.detectChanges();

  }));
@alexey-kozlenkov

This comment has been minimized.

Copy link

alexey-kozlenkov commented Aug 15, 2018

Wondering what's the status of this issue?
As far as I get, tick inside fakeAsync still don't work as expected in case of using delay operator.

discardPeriodicTasks prevents 'Error: 1 periodic timer(s) still in the queue.' but test case is still failing.
I also tried @rvmladenov's workaround with flush but this didn't help too.

The one working is replacing delay with timer as @IliaVolk suggests.

@MuraliM

This comment has been minimized.

Copy link

MuraliM commented Sep 13, 2018

I have tried with fakeAsync and flush/tick. Nothing worked for me. As @Goodwine suggested, I have changed to use Jasmin done. Even updated Zone.js to the latest version 0.8.26.

I still get the error Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

Here is my integration testing code

 it('Should have header set to -> Edit Employee - Alex', (done) => {

  fixture.detectChanges();
  

  fixture.whenStable().then(() => {
    
    let componentHeaderElement = fixture.debugElement.query(By.directive(ComponentHeaderComponent));
    const headerText = componentHeaderElement.query(By.css("#component-title")).nativeElement.textContent;
   
    expect(componentInstance.componentForm.form.pristine).toBeTruthy();
    expect(headerText).toContain("Edit Employee - Alex");

    done();

  });

});
"jasmine-core": "2.8.0",
  "karma": "^2.0.0",
  "karma-chrome-launcher": "^2.2.0",
  "karma-cli": "^1.0.1",
  "karma-coverage": "^1.1.1",
  "karma-jasmine": "^1.1.0",
  "karma-jasmine-html-reporter": "0.2.2",
  "karma-junit-reporter": "^1.2.0",
  "karma-sonarqube-unit-reporter": "0.0.15",
  "karma-teamcity-reporter": "^1.1.0",
  "@angular/common": "^4.4.0",
  .... other angular libs
@Goodwine

This comment has been minimized.

Copy link
Contributor

Goodwine commented Sep 13, 2018

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.

@vicneanschi

This comment has been minimized.

Copy link

vicneanschi commented Sep 25, 2018

This is solved by @JiaLiPassion. Just do the setup as described in angular/zone.js#1009 (comment) if using Angular 5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment