在 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
实例的 now
和 schedule
方法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)
在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
实例的 now
和 schedule
方法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)