Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing and mocking lettable operators in RxJS 5.5

Before lettable operator, I did a helper to modify debounceTime method, so it uses a TestScheduler:

export function mockDebounceTime(
    scheduler: TestScheduler,
    overrideTime: number,
): void {
    const originalDebounce = Observable.prototype.debounceTime;

    spyOn(Observable.prototype, 'debounceTime').and.callFake(function(
        time: number,
    ): void {
        return originalDebounce.call(
            this,
            overrideTime,
            scheduler,
        );
    });
}

So the test of the following Observable was easy:

@Effect()
public filterUpdated$ = this.actions$
    .ofType(UPDATE_FILTERS)
    .debounceTime(DEFAULT_DEBOUNCE_TIME)
    .mergeMap(action => [...])

With lettable operators, the filterUpdated$ Observable is written like that:

@Effect()
public filterUpdated$ = this.actions$
    .ofType(UPDATE_FILTERS)
    .pipe(
        debounceTime(DEFAULT_DEBOUNCE_TIME),
        mergeMap(action => [...])
    );

I cannot patch the debounceTime operator anymore ! How can I pass the testScheduler to the debounceTime operator ?

like image 934
Guillaume Nury Avatar asked Nov 17 '17 16:11

Guillaume Nury


3 Answers

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();
  });
});
like image 85
Richard Matsen Avatar answered Nov 10 '22 00:11

Richard Matsen


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
  });
});
like image 21
Gerard Sans Avatar answered Nov 09 '22 23:11

Gerard Sans


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();
});
like image 1
cartant Avatar answered Nov 10 '22 00:11

cartant