【问题标题】:Testing and mocking lettable operators in RxJS 5.5在 RxJS 5.5 中测试和模拟 lettable 运算符
【发布时间】:2018-05-01 12:38:24
【问题描述】:

在 lettable 操作符之前,我做了一个 helper 来修改 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 运算符?

【问题讨论】:

  • 你可以看看example app form NgRx。在效果中使用 DI 添加调度程序,并在测试时提供不同的调度程序。
  • 但是我不太喜欢这种方法,不想将代码添加到仅用于测试的效果中,这种方法意味着在测试效果时使用 TestBed。当我有时间时,我会寻找一些替代品。
  • 感谢链接。我们不应该为了测试目的而修改我们的代码:(
  • 作为一个get arround,我在我的类中添加了两个方法,所以我可以监视它们。但我不喜欢那个解决方案...
  • 回到这个问题,用我发现的东西写下答案,这与@cartan 下面写的非常相似,改变了异步调度程序的工作方式,因为这是 RxJs 默认使用的。不是很好,但这是我发现的唯一不涉及更改效果代码的方法。

标签: typescript rxjs5 ngrx-effects


【解决方案1】:

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

Lettable 运算符(哎呀,现在应该称它们为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.pipe 以一种不知道确切参数位置的方式(这样它只会检测到“debounceTime”被给出并相应地包装它)。
【解决方案2】:

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

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

【讨论】:

  • 您好,问题是仅在测试期间注入此自定义调度程序。在 RxJS 5.4 上模拟 debounceTime 很“容易”,但现在,@Adrian-Fâciu 似乎指出了唯一干净的解决方案
  • 你希望你的依赖明确。这是要走的路。
  • 这对我有用,但我必须确保调度程序是链上某处的可用提供程序。 { provide: Scheduler, useValue: Scheduler }
【解决方案3】:

如果难以将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();
});

【讨论】:

【解决方案4】:

更新:如果您返回一组操作并且想要验证所有操作,请删除

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

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

【讨论】:

  • 谢谢!这真的有帮助!
【解决方案5】:

我对上面的答案有一些问题(在 Observable.prototype 上不能有多个间谍,...),与我相关的只是嘲笑“debounceTime”,所以我将实际的 debounceTime(例如 filterTextDebounceTime = 200)移动到组件和规范的“beforeEach”中的一个变量我将 component.filterTextDebounceTime 设置为 0,因此 debounceTime 同步/阻塞工作。

【讨论】:

  • (这篇文章似乎没有为问题提供quality answer。请编辑您的答案并包含最终的源代码解决方案,或者只是将其作为对问题的评论发布)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-05-13
  • 2018-05-28
  • 2018-06-10
  • 2018-04-01
  • 2018-08-14
  • 2020-12-12
相关资源
最近更新 更多