在 RxJS 5.5 中测试和模拟 lettable operators

Testing and mocking lettable operators in RxJS 5.5

在lettable operator之前,我做了一个修改debounceTime方法的助手,所以它使用了一个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,
        );
    });
}

所以下面的 Observable 的测试很简单:

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

有了 lettable 运算符,filterUpdated$ Observable 是这样写的:

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

我不能再给 debounceTime 运算符打补丁了!如何将 testScheduler 传递给 debounceTime 运算符?

您可以使用接受自定义调度程序的第二个参数。

  debounceTime(DEFAULT_DEBOUNCE_TIME, rxTestScheduler),

所有代码

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 => [...])
    );
}

然后在测试

describe('Service: EffectsService', () => {
  //setup
  beforeEach(() => TestBed.configureTestingModule({
    EffectsService, 
    { provide: Scheduler, useValue: rxTestScheduler} ]
  }));

  //specs
  it('should update filters using debounce', inject([EffectsService], service => {
    // your test
  });
});

如果难以将 TestScheduler 实例注入或传递给您的操作员,最简单的解决方案是重新绑定 AsyncScheduler 实例的 nowschedule 方法TestScheduler 实例。

您可以手动执行此操作:

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

或者您可以使用 sinon 存根:

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

由于 .pipe() 仍在 Observable 原型上,您可以在其上使用模拟技术。

可出租运算符(糟糕,现在应该称它们为 pipeable operators)可以在模拟管道中按原样使用。

这是我在干净的 CLI 应用程序的 app.component.spec.ts 中使用的代码。请注意,它可能不是 TestScheduler 的最佳使用,但显示了原理。

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

我对上面的答案有一些问题(在 Observable.prototype 上不能有多个间谍,...),对我来说相关的只是嘲笑 "debounceTime" 所以我移动了实际的 debounceTime(例如 filterTextDebounceTime = 200) 到组件和规范 "beforeEach" 中的变量 我将 component.filterTextDebounceTime 设置为 0 所以 debounceTime 正在工作 synchronously/blocking.

更新: 如果您 return 一系列操作并且想要验证所有操作,请删除

.pipe(throttleTime(1, myScheduler))

并且您可以使用 jasmine-marbles 中的 getTestScheduler 而不是创建您自己的调度程序。

import { getTestScheduler } from 'jasmine-marbles';

因此测试可能如下所示:

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

我一直在努力使用 debounceTime 测试 ngrx 效果。现在好像有些变化了。我在这里关注文档:https://github.com/ReactiveX/rxjs/blob/master/doc/marble-testing.md

这是我的测试结果:

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

实际代码中不需要使用scheduler,例如:no debounceTime(200, this.scheduler)