Testing and mocking lettable operators in RxJS 5.5

99封情书 提交于 2019-11-28 00:04:41

问题


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 ?


回答1:


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



回答2:


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



回答3:


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



回答4:


Update: should you return an array of actions and you want to verify all of them, remove the

.pipe(throttleTime(1, myScheduler))

And you can use getTestScheduler from jasmine-marbles instead of creating your own scheduler.

import { getTestScheduler } from 'jasmine-marbles';

So a test could look like this:

  it('should pass', () => {
    getTestScheduler().run((helpers) => {
      const action = new fromAppActions.LoadApps();
      const completion1 = new fromAppActions.FetchData();
      const completion2 = new fromAppActions.ShowWelcome();
      actions$ = helpers.hot('-a', { a: action });
      helpers
        .expectObservable(effects.load$)
        .toBe('300ms -(bc)', { b: completion1, c: completion2 });
    });
  });

I was struggling with testing a ngrx effect with debounceTime. It seems things have changed a bit now. I followed the doc here: https://github.com/ReactiveX/rxjs/blob/master/doc/marble-testing.md

Here is what my test looks like:

    describe('someEffect$', () => {
      const myScheduler = new TestScheduler((a, b) => expect(a).toEqual(b));
      it('should test', () => {
        myScheduler.run((helpers) => {
          const action = new fromActions.SomeAction();
          const completion = new fromActions.SomeCompleteAction(someData);
          actions$.stream = helpers.hot('-a', { a: action });

          helpers
            .expectObservable(effects.someEffect$.pipe(throttleTime(1, myScheduler)))
            .toBe('200ms -(b)', { b: completion });
        });
      });
    });

There is no need to use scheduler in the actual code, e.g.: no debounceTime(200, this.scheduler)




回答5:


I had some problems with the answers above (can't have multiple spies on Observable.prototype, ...), relevant for me was only mocking "debounceTime" so I moved the actual debounceTime (e.g. filterTextDebounceTime = 200) to an variable in the component and in the spec's "beforeEach" I'm setting the component.filterTextDebounceTime to 0 so debounceTime is working synchronously/blocking.



来源:https://stackoverflow.com/questions/47355666/testing-and-mocking-lettable-operators-in-rxjs-5-5

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!