Testing and mocking lettable operators in RxJS 5.5
Since .pipe()
is still on the Observable prototype, you can use your mocking technique on it.
Lettable operators (oops, now supposed to call them pipeable operators) can be used as-is within the mock pipe.
This is the code I used in app.component.spec.ts of a clean CLI application. Note, it's probably not best use of TestScheduler but shows the principle.
import { TestBed, async } from '@angular/core/testing';import { AppComponent } from './app.component';import { Observable } from 'rxjs/Observable';import { debounceTime, take, tap } from 'rxjs/operators';import { TestScheduler } from 'rxjs/Rx';export function mockPipe(...mockArgs) { const originalPipe = Observable.prototype.pipe; spyOn(Observable.prototype, 'pipe').and.callFake(function(...actualArgs) { const args = [...actualArgs]; mockArgs.forEach((mockArg, index) => { if(mockArg) { args[index] = mockArg; } }); return originalPipe.call(this, ...args); });}describe('AppComponent', () => { it('should test lettable operators', () => { const scheduler = new TestScheduler(null); // Leave first tap() as-is but mock debounceTime() mockPipe(null, debounceTime(300, scheduler)); const sut = Observable.timer(0, 300).take(10) .pipe( tap(x => console.log('before ', x)), debounceTime(300), tap(x => console.log('after ', x)), take(4), ); sut.subscribe((data) => console.log(data)); scheduler.flush(); });});
You can use the second argument that accepts a custom Scheduler.
debounceTime(DEFAULT_DEBOUNCE_TIME, rxTestScheduler),
All code
import { Scheduler } from 'rxjs/scheduler/Scheduler';import { asap } from 'rxjs/scheduler/asap';@Injectable()export class EffectsService { constructor(private scheduler: Scheduler = asap) { } @Effect() public filterUpdated$ = this.actions$ .ofType(UPDATE_FILTERS) .pipe( debounceTime(DEFAULT_DEBOUNCE_TIME, this.scheduler), mergeMap(action => [...]) );}
Then on test
describe('Service: EffectsService', () => { //setup beforeEach(() => TestBed.configureTestingModule({ EffectsService, { provide: Scheduler, useValue: rxTestScheduler} ] })); //specs it('should update filters using debounce', inject([EffectsService], service => { // your test });});
If it's difficult to inject or pass the TestScheduler
instance to your operators, this simplest solution is to rebind the now
and schedule
methods of the AsyncScheduler
instance to those of the TestScheduler
instance.
You could either do this manually:
import { async } from "rxjs/Scheduler/async";it("should rebind to the test scheduler", () => { const testScheduler = new TestScheduler(); async.now = () => testScheduler.now(); async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state); // test something delete async.now; delete async.schedule;});
Or you could use a sinon
stub:
import { async } from "rxjs/Scheduler/async";import * as sinon from "sinon";it("should rebind to the test scheduler", () => { const testScheduler = new TestScheduler(); const stubNow = sinon.stub(async, "now").callsFake( () => testScheduler.now() ); const stubSchedule = sinon.stub(async, "schedule").callsFake( (work, delay, state) => testScheduler.schedule(work, delay, state) ); // test something stubNow.restore(); stubSchedule.restore();});