延迟测试NGRX效果
Testing NGRX effect with delay
我想测试如下效果:
- 如果 LoadEntriesSucces 操作被调度,效果开始
- 它等待 5 秒
- 5 秒后发送 http 请求
- 响应到达时,将调度新操作(取决于响应是成功还是错误)。
效果代码如下:
@Effect()
continuePollingEntries$ = this.actions$.pipe(
ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
delay(5000),
switchMap(() => {
return this.subnetBrowserService.getSubnetEntries().pipe(
map((entries) => {
return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
}),
catchError((error) => {
return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
}),
);
}),
);
我想测试的是5秒后是否派发效果:
it('should dispatch action after 5 seconds', () => {
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
expect(effects.continuePollingEntries$).toBeObservable(expected);
});
但是这个测试对我不起作用。测试输出如下所示:
Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
我应该怎么做才能让这个测试成功?
您可以使用来自 jasmine
的 done
回调
it('should dispatch action after 5 seconds', (done) => {
const resMock = 'resMock';
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
effects.continuePollingEntries$.subscribe((res)=>{
expect(res).toEqual(resMock);
done()
})
});
第二种表示法不适用于 jasmine-marbles
,请改用破折号:
const expected = cold('------b ', { b: completion });
你需要做 3 件事
1- 在你的 beforeEach
中,你需要覆盖 RxJs 的内部调度程序,如下所示:
import { async } from 'rxjs/internal/scheduler/async';
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
beforeEach(() => {.....
const testScheduler = getTestScheduler();
async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})
2-用delayWhen替换delay,如下:
delayWhen(_x => (true ? interval(50) : of(undefined)))
3- 使用帧,我不太确定如何为此使用秒数,所以我使用了帧。每帧为10ms。因此,例如我上面的延迟是 50 毫秒,我的帧是 -b,所以这是预期的 10 毫秒 + 我需要另外 50 毫秒所以这等于额外的 5 帧 ------b 如下:
const expected = cold('------b ', { b: outcome });
就像另一个答案中提到的那样,一种测试该效果的方法是使用 TestScheduler,但它可以通过更简单的方式完成。
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
例如,让我们对以下效果进行单元测试:
effectWithDelay$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromFooActions.doSomething),
delay(5000),
switchMap(({ payload }) => {
const { someData } = payload;
return this.fooService.someMethod(someData).pipe(
map(() => {
return fromFooActions.doSomethingSuccess();
}),
catchError(() => {
return of(fromFooActions.doSomethinfError());
}),
);
}),
);
});
效果仅在初始操作后等待 5 秒,并调用一个服务,该服务随后会发送成功或错误操作。对该效果进行单元测试的代码如下:
import { TestBed } from "@angular/core/testing";
import { provideMockActions } from "@ngrx/effects/testing";
import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";
import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";
// ...
describe("FooEffects", () => {
let actions$: Observable<unknown>;
let testScheduler: TestScheduler; // <-- instance of the test scheduler
let effects: FooEffects;
let fooServiceMock: jasmine.SpyObj<FooService>;
beforeEach(() => {
// Initialize the TestScheduler instance passing a function to
// compare if two objects are equal
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
TestBed.configureTestingModule({
imports: [],
providers: [
FooEffects,
provideMockActions(() => actions$),
// Mock the service so that we can test if it was called
// and if the right data was sent
{
provide: FooService,
useValue: jasmine.createSpyObj("FooService", {
someMethod: jasmine.createSpy(),
}),
},
],
});
effects = TestBed.inject(FooEffects);
fooServiceMock = TestBed.inject(FooService);
});
describe("effectWithDelay$", () => {
it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
const someDataMock = { someData: Math.random() * 100 };
const initialAction = fromFooActions.doSomething(someDataMock);
const expectedAction = fromFooActions.doSomethingSuccess();
testScheduler.run((helpers) => {
// When the code inside this callback is being executed, any operator
// that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
// **automatically** use the TestScheduler instead, so that we have
// "virtual time". You do not need to pass the TestScheduler to them,
// like in the past.
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
const { hot, cold, expectObservable } = helpers;
// Actions // -a-
// Service // -b|
// Results // 5s --c
// Actions
actions$ = hot("-a-", { a: initialAction });
// Service
fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
// Results
expectObservable(effects.effectWithDelay$).toBe("5s --c", {
c: expectedAction,
});
});
// This needs to be outside of the run() callback
// since it's executed synchronously :O
expect(fooServiceMock.someMethod).toHaveBeenCalled();
expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
});
});
});
请注意,在代码中我使用 expectObservable
来测试使用来自 TestScheduler 实例的“虚拟时间”的效果。
我想测试如下效果:
- 如果 LoadEntriesSucces 操作被调度,效果开始
- 它等待 5 秒
- 5 秒后发送 http 请求
- 响应到达时,将调度新操作(取决于响应是成功还是错误)。
效果代码如下:
@Effect()
continuePollingEntries$ = this.actions$.pipe(
ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
delay(5000),
switchMap(() => {
return this.subnetBrowserService.getSubnetEntries().pipe(
map((entries) => {
return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
}),
catchError((error) => {
return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
}),
);
}),
);
我想测试的是5秒后是否派发效果:
it('should dispatch action after 5 seconds', () => {
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
expect(effects.continuePollingEntries$).toBeObservable(expected);
});
但是这个测试对我不起作用。测试输出如下所示:
Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
我应该怎么做才能让这个测试成功?
您可以使用来自 jasmine
的done
回调
it('should dispatch action after 5 seconds', (done) => {
const resMock = 'resMock';
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
effects.continuePollingEntries$.subscribe((res)=>{
expect(res).toEqual(resMock);
done()
})
});
第二种表示法不适用于 jasmine-marbles
,请改用破折号:
const expected = cold('------b ', { b: completion });
你需要做 3 件事
1- 在你的 beforeEach
中,你需要覆盖 RxJs 的内部调度程序,如下所示:
import { async } from 'rxjs/internal/scheduler/async';
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
beforeEach(() => {.....
const testScheduler = getTestScheduler();
async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})
2-用delayWhen替换delay,如下:
delayWhen(_x => (true ? interval(50) : of(undefined)))
3- 使用帧,我不太确定如何为此使用秒数,所以我使用了帧。每帧为10ms。因此,例如我上面的延迟是 50 毫秒,我的帧是 -b,所以这是预期的 10 毫秒 + 我需要另外 50 毫秒所以这等于额外的 5 帧 ------b 如下:
const expected = cold('------b ', { b: outcome });
就像另一个答案中提到的那样,一种测试该效果的方法是使用 TestScheduler,但它可以通过更简单的方式完成。
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
例如,让我们对以下效果进行单元测试:
effectWithDelay$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromFooActions.doSomething),
delay(5000),
switchMap(({ payload }) => {
const { someData } = payload;
return this.fooService.someMethod(someData).pipe(
map(() => {
return fromFooActions.doSomethingSuccess();
}),
catchError(() => {
return of(fromFooActions.doSomethinfError());
}),
);
}),
);
});
效果仅在初始操作后等待 5 秒,并调用一个服务,该服务随后会发送成功或错误操作。对该效果进行单元测试的代码如下:
import { TestBed } from "@angular/core/testing";
import { provideMockActions } from "@ngrx/effects/testing";
import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";
import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";
// ...
describe("FooEffects", () => {
let actions$: Observable<unknown>;
let testScheduler: TestScheduler; // <-- instance of the test scheduler
let effects: FooEffects;
let fooServiceMock: jasmine.SpyObj<FooService>;
beforeEach(() => {
// Initialize the TestScheduler instance passing a function to
// compare if two objects are equal
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
TestBed.configureTestingModule({
imports: [],
providers: [
FooEffects,
provideMockActions(() => actions$),
// Mock the service so that we can test if it was called
// and if the right data was sent
{
provide: FooService,
useValue: jasmine.createSpyObj("FooService", {
someMethod: jasmine.createSpy(),
}),
},
],
});
effects = TestBed.inject(FooEffects);
fooServiceMock = TestBed.inject(FooService);
});
describe("effectWithDelay$", () => {
it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
const someDataMock = { someData: Math.random() * 100 };
const initialAction = fromFooActions.doSomething(someDataMock);
const expectedAction = fromFooActions.doSomethingSuccess();
testScheduler.run((helpers) => {
// When the code inside this callback is being executed, any operator
// that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
// **automatically** use the TestScheduler instead, so that we have
// "virtual time". You do not need to pass the TestScheduler to them,
// like in the past.
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
const { hot, cold, expectObservable } = helpers;
// Actions // -a-
// Service // -b|
// Results // 5s --c
// Actions
actions$ = hot("-a-", { a: initialAction });
// Service
fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
// Results
expectObservable(effects.effectWithDelay$).toBe("5s --c", {
c: expectedAction,
});
});
// This needs to be outside of the run() callback
// since it's executed synchronously :O
expect(fooServiceMock.someMethod).toHaveBeenCalled();
expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
});
});
});
请注意,在代码中我使用 expectObservable
来测试使用来自 TestScheduler 实例的“虚拟时间”的效果。