Angular Marble 测试中的 NgRx 效果错误:预期 $.length = 0 等于 2。/预期 $[0] = 未定义等于对象
Angular NgRx Effect errors in Marble Testing: Expected $.length = 0 to equal 2. / Expected $[0] = undefined to equal Object
我有一个使用 Angular 和 NgRx 的应用程序,但我很难使用 Marble 测试来测试我的效果。
我得到的错误是:
Expected $.length = 0 to equal 2.
Expected $[0] = undefined to equal Object({ frame: 10, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'C', value: undefined, error: undefined, hasValue: false }) }).
效果如下:
@Injectable()
export class OrderLogisticStatusEffects {
loadOrdersLogisticStatus$ = createEffect(() =>
this.actions$.pipe(
ofType(LOAD_ORDERS_LOGISTIC_STATUS),
withLatestFrom(this.store$.pipe(select(orderLogisticsStatusPollingIntervalSelector))),
switchMap(([action, pollingInterval]) =>
timer(0, pollingInterval).pipe(
withLatestFrom(this.store$.pipe(select(selectedCompanySelector))),
switchMap(([timerNum, company]) => this.loadOrdersLogisticStatus(pollingInterval, company))
)
)
)
);
constructor(private actions$: Actions, private orderLogisticStatusService: OrderLogisticStatusService, private store$: Store<AppState>) {}
private loadOrdersLogisticStatus(
pollingInterval: number,
company: Company
): Observable<LoadOrderLogisticStatusSuccess | LoadOrderLogisticStatusFail> {
if (!company?.logisticsToken) {
return of(new LoadOrderLogisticStatusFail(new Error('No company selected')));
}
this.orderLogisticStatusService.getOrdersStatus(company.logisticsToken).pipe(
timeout(pollingInterval),
map((result) => new LoadOrderLogisticStatusSuccess(result)),
catchError((error) => {
if (error.name === 'TimeoutError') {
console.warn('Timeout error while loadin logistic status service', error);
} else {
console.error('Error loading order logistic status', error);
Sentry.captureException(error);
}
return of(new LoadOrderLogisticStatusFail(error));
})
);
}
}
这是我的测试:
fdescribe('Order Logistic Status Effect', () => {
let actions$: Observable<Action>;
let effects: OrderLogisticStatusEffects;
describe('With a selected company', () => {
beforeEach(() => {
const mockState = {
ordersLogisticStatus: {
pollingInterval: 10,
},
company: {
selectedCompany: {
logisticsToken: 'ey.xxxx.yyyy',
},
},
};
TestBed.configureTestingModule({
providers: [
{ provide: OrderLogisticStatusService, useValue: jasmine.createSpyObj('orderLogisticsStatusServiceSpy', ['getOrdersStatus']) },
OrderLogisticStatusEffects,
provideMockActions(() => actions$),
provideMockStore({
selectors: [
{
selector: orderLogisticsStatusPollingIntervalSelector,
value: 30,
},
{
selector: selectedCompanySelector,
value: {
logisticsToken: 'ey.xxxx.yyy',
},
},
],
}),
],
});
effects = TestBed.inject<OrderLogisticStatusEffects>(OrderLogisticStatusEffects);
});
it('should sucessfully load the orders logistics status', () => {
const service: jasmine.SpyObj<OrderLogisticStatusService> = TestBed.inject(OrderLogisticStatusService) as any;
service.getOrdersStatus.and.returnValue(cold('-a|', { a: mockData }));
actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
const expected = hot('-a|', {
a: new LoadOrderLogisticStatusSuccess(mockData),
});
getTestScheduler().flush();
expect(effects.loadOrdersLogisticStatus$).toBeObservable(expected);
});
});
});
const mockData = {
1047522: {
status: 0,
partner: {
id: 1,
},
eta: '2020-06-09 10:00',
pickupEta: '2020-06-09 12:00',
},
};
问题似乎出在我的服务模拟上。尽管我将它配置为 return 冷可观察对象,但它似乎是 return 未定义的。
谁能帮帮我?
有几个问题:
首先,loadOrdersLogisticStatus
缺少 return:
loadOrdersLogisticStatus (/* ... */) {
return this.orderLogisticStatusService.getOrdersStatus(/* ... */)
}
然后,我发现 jasmine-marbles
不会自动设置 AsyncScheduler.delegate
,而不是 TestScheduler.run
:
run<T>(callback: (helpers: RunHelpers) => T): T {
const prevFrameTimeFactor = TestScheduler.frameTimeFactor;
const prevMaxFrames = this.maxFrames;
TestScheduler.frameTimeFactor = 1;
this.maxFrames = Infinity;
this.runMode = true;
AsyncScheduler.delegate = this;
/* ... */
}
这很重要,因为在使用弹珠时,一切都是同步。但是在您的实现中,有一个 timer(0, pollingInterval)
可观察对象,默认情况下,它使用 AsyncScheduler
。如果不设置 AsyncScheduler.delegate
,我们将进行异步操作,我认为这是主要问题。
因此,为了设置 delegate
属性,我在 beforeEach()
中添加了这一行:
AsyncScheduler.delegate = getTestScheduler();
最后,我觉得你的论断有点小问题。您的效果 属性 似乎从未完成,而且您还在使用 timer(0, pollingInterval)
。所以我认为您现在可以做的是添加 take(N)
运算符以针对 N
排放进行测试:
it("should sucessfully load the orders logistics status", () => {
const service: jasmine.SpyObj<OrderLogisticStatusService> = TestBed.inject(OrderLogisticStatusService) as any;
service.getOrdersStatus.and.callFake(() => cold('-a|', { a: mockData }));
actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
const expected = hot('-a--(b|)', {
a: new LoadOrderLogisticStatusSuccess(mockData),
b: new LoadOrderLogisticStatusSuccess(mockData),
});
expect(effects.loadOrdersLogisticStatus$.pipe(take(2))).toBeObservable(expected);
});
'-a--(b|)'
- a
在第 10 帧发送,b
+ complete
通知(由于 take
)在 40th
帧,因为 pollingInterval
是 30
并且在安排第二个通知 (b
) 时当前帧将是 10
。
Andrei 的解决方案确实适用于 Stack Blitz,但它在我自己的环境中不起作用,我认为这是因为一些导入混淆。但它背后的想法:jasmine-marbles
的 TestScheduler
并没有取代 RxJs 的 AsyncScheduler
是正确的。这是我的问题(除了忘记在 loadOrdersLogisticStatus
函数中添加 return
语句)。
所以为了让一切正常,我必须做两件事:
首先,我必须手动将效果的调度程序设置为 TestScheduler
。当我这样做时,我终于能够看到 timer
observable 正在发出结果,而不是像以前那样 undefined
。
然而我又遇到了另一个问题。 timer
不会停止发出,所以我得到了一个像这样的长错误流:
Expected $.length = 26 to equal 2.
Unexpected $[2] = Object({ frame: 60, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[3] = Object({ frame: 90, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[4] = Object({ frame: 120, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[5] = Object({ frame: 150, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[6] = Object({ frame: 180, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[7] = Object({ frame: 210, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[8] = Object({ frame: 240, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[9] = Object({ frame: 270, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
前两个值没问题,其他的就不行。所以我做的第二件事是使用 takeUntil
运算符取消订阅 timer
一个测试完成(我不确定这是否是最好的方法,因为我绝不能使用这个操作员在测试之外,否则我的轮询将停止)。
这是代码的最终工作版本:
效果:
@Injectable()
export class OrderLogisticStatusEffects {
loadOrdersLogisticStatus$ = createEffect(() => ({ scheduler = asyncScheduler, stopTimer = EMPTY } = {}) =>
this.actions$.pipe(
ofType(LOAD_ORDERS_LOGISTIC_STATUS),
withLatestFrom(this.store$.pipe(select(orderLogisticsStatusPollingIntervalSelector))),
switchMap(([action, pollingInterval]) =>
timer(0, pollingInterval, scheduler).pipe(
takeUntil(stopTimer),
withLatestFrom(this.store$.pipe(select(selectedCompanySelector))),
switchMap(([timerNum, company]) => this.loadOrdersLogisticStatus(pollingInterval, company))
)
)
)
);
constructor(private actions$: Actions, private orderLogisticStatusService: OrderLogisticStatusService, private store$: Store<AppState>) {}
private loadOrdersLogisticStatus(
pollingInterval: number,
company: Company
): Observable<LoadOrderLogisticStatusSuccess | LoadOrderLogisticStatusFail> {
if (!company?.logisticsToken) {
return of(new LoadOrderLogisticStatusFail(new Error('No company selected')));
}
return this.orderLogisticStatusService.getOrdersStatus(company.logisticsToken).pipe(
timeout(pollingInterval),
map((result) => new LoadOrderLogisticStatusSuccess(result)),
catchError((error) => {
if (error.name === 'TimeoutError') {
console.warn('Timeout error while loadin logistic status service', error);
} else {
console.error('Error loading order logistic status', error);
Sentry.captureException(error);
}
return of(new LoadOrderLogisticStatusFail(error));
})
);
}
}
测试:
fdescribe('Order Logistic Status Effect', () => {
let actions$: Observable<Action>;
let effects: OrderLogisticStatusEffects;
describe('With a selected company', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: OrderLogisticStatusService, useValue: jasmine.createSpyObj('orderLogisticsStatusServiceSpy', ['getOrdersStatus']) },
OrderLogisticStatusEffects,
provideMockActions(() => actions$),
provideMockStore({
selectors: [
{
selector: orderLogisticsStatusPollingIntervalSelector,
value: 30,
},
{
selector: selectedCompanySelector,
value: {
logisticsToken: 'ey.xxxx.yyy',
},
},
],
}),
],
});
effects = TestBed.inject<OrderLogisticStatusEffects>(OrderLogisticStatusEffects);
});
it('should sucessfully load the orders logistics status', () => {
const service = TestBed.inject(OrderLogisticStatusService) as jasmine.SpyObj<OrderLogisticStatusService>;
service.getOrdersStatus.and.callFake(() => cold('a|', { a: mockData }));
actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
const expected = hot('a--b', {
a: new LoadOrderLogisticStatusSuccess(mockData),
b: new LoadOrderLogisticStatusSuccess(mockData),
});
const stopTimer = hot('----a', { a: 'stop' });
const testScheduler = getTestScheduler();
expect(effects.loadOrdersLogisticStatus$({ scheduler: testScheduler, stopTimer })).toBeObservable(expected);
});
});
});
const mockData = {
1047522: {
status: 0,
partner: {
id: 1,
slug: 'loggi',
},
eta: '2020-06-09 10:00',
pickupEta: '2020-06-09 12:00',
},
};
我有一个使用 Angular 和 NgRx 的应用程序,但我很难使用 Marble 测试来测试我的效果。
我得到的错误是:
Expected $.length = 0 to equal 2.
Expected $[0] = undefined to equal Object({ frame: 10, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'C', value: undefined, error: undefined, hasValue: false }) }).
效果如下:
@Injectable()
export class OrderLogisticStatusEffects {
loadOrdersLogisticStatus$ = createEffect(() =>
this.actions$.pipe(
ofType(LOAD_ORDERS_LOGISTIC_STATUS),
withLatestFrom(this.store$.pipe(select(orderLogisticsStatusPollingIntervalSelector))),
switchMap(([action, pollingInterval]) =>
timer(0, pollingInterval).pipe(
withLatestFrom(this.store$.pipe(select(selectedCompanySelector))),
switchMap(([timerNum, company]) => this.loadOrdersLogisticStatus(pollingInterval, company))
)
)
)
);
constructor(private actions$: Actions, private orderLogisticStatusService: OrderLogisticStatusService, private store$: Store<AppState>) {}
private loadOrdersLogisticStatus(
pollingInterval: number,
company: Company
): Observable<LoadOrderLogisticStatusSuccess | LoadOrderLogisticStatusFail> {
if (!company?.logisticsToken) {
return of(new LoadOrderLogisticStatusFail(new Error('No company selected')));
}
this.orderLogisticStatusService.getOrdersStatus(company.logisticsToken).pipe(
timeout(pollingInterval),
map((result) => new LoadOrderLogisticStatusSuccess(result)),
catchError((error) => {
if (error.name === 'TimeoutError') {
console.warn('Timeout error while loadin logistic status service', error);
} else {
console.error('Error loading order logistic status', error);
Sentry.captureException(error);
}
return of(new LoadOrderLogisticStatusFail(error));
})
);
}
}
这是我的测试:
fdescribe('Order Logistic Status Effect', () => {
let actions$: Observable<Action>;
let effects: OrderLogisticStatusEffects;
describe('With a selected company', () => {
beforeEach(() => {
const mockState = {
ordersLogisticStatus: {
pollingInterval: 10,
},
company: {
selectedCompany: {
logisticsToken: 'ey.xxxx.yyyy',
},
},
};
TestBed.configureTestingModule({
providers: [
{ provide: OrderLogisticStatusService, useValue: jasmine.createSpyObj('orderLogisticsStatusServiceSpy', ['getOrdersStatus']) },
OrderLogisticStatusEffects,
provideMockActions(() => actions$),
provideMockStore({
selectors: [
{
selector: orderLogisticsStatusPollingIntervalSelector,
value: 30,
},
{
selector: selectedCompanySelector,
value: {
logisticsToken: 'ey.xxxx.yyy',
},
},
],
}),
],
});
effects = TestBed.inject<OrderLogisticStatusEffects>(OrderLogisticStatusEffects);
});
it('should sucessfully load the orders logistics status', () => {
const service: jasmine.SpyObj<OrderLogisticStatusService> = TestBed.inject(OrderLogisticStatusService) as any;
service.getOrdersStatus.and.returnValue(cold('-a|', { a: mockData }));
actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
const expected = hot('-a|', {
a: new LoadOrderLogisticStatusSuccess(mockData),
});
getTestScheduler().flush();
expect(effects.loadOrdersLogisticStatus$).toBeObservable(expected);
});
});
});
const mockData = {
1047522: {
status: 0,
partner: {
id: 1,
},
eta: '2020-06-09 10:00',
pickupEta: '2020-06-09 12:00',
},
};
问题似乎出在我的服务模拟上。尽管我将它配置为 return 冷可观察对象,但它似乎是 return 未定义的。
谁能帮帮我?
有几个问题:
首先,loadOrdersLogisticStatus
缺少 return:
loadOrdersLogisticStatus (/* ... */) {
return this.orderLogisticStatusService.getOrdersStatus(/* ... */)
}
然后,我发现 jasmine-marbles
不会自动设置 AsyncScheduler.delegate
,而不是 TestScheduler.run
:
run<T>(callback: (helpers: RunHelpers) => T): T {
const prevFrameTimeFactor = TestScheduler.frameTimeFactor;
const prevMaxFrames = this.maxFrames;
TestScheduler.frameTimeFactor = 1;
this.maxFrames = Infinity;
this.runMode = true;
AsyncScheduler.delegate = this;
/* ... */
}
这很重要,因为在使用弹珠时,一切都是同步。但是在您的实现中,有一个 timer(0, pollingInterval)
可观察对象,默认情况下,它使用 AsyncScheduler
。如果不设置 AsyncScheduler.delegate
,我们将进行异步操作,我认为这是主要问题。
因此,为了设置 delegate
属性,我在 beforeEach()
中添加了这一行:
AsyncScheduler.delegate = getTestScheduler();
最后,我觉得你的论断有点小问题。您的效果 属性 似乎从未完成,而且您还在使用 timer(0, pollingInterval)
。所以我认为您现在可以做的是添加 take(N)
运算符以针对 N
排放进行测试:
it("should sucessfully load the orders logistics status", () => {
const service: jasmine.SpyObj<OrderLogisticStatusService> = TestBed.inject(OrderLogisticStatusService) as any;
service.getOrdersStatus.and.callFake(() => cold('-a|', { a: mockData }));
actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
const expected = hot('-a--(b|)', {
a: new LoadOrderLogisticStatusSuccess(mockData),
b: new LoadOrderLogisticStatusSuccess(mockData),
});
expect(effects.loadOrdersLogisticStatus$.pipe(take(2))).toBeObservable(expected);
});
'-a--(b|)'
- a
在第 10 帧发送,b
+ complete
通知(由于 take
)在 40th
帧,因为 pollingInterval
是 30
并且在安排第二个通知 (b
) 时当前帧将是 10
。
Andrei 的解决方案确实适用于 Stack Blitz,但它在我自己的环境中不起作用,我认为这是因为一些导入混淆。但它背后的想法:jasmine-marbles
的 TestScheduler
并没有取代 RxJs 的 AsyncScheduler
是正确的。这是我的问题(除了忘记在 loadOrdersLogisticStatus
函数中添加 return
语句)。
所以为了让一切正常,我必须做两件事:
首先,我必须手动将效果的调度程序设置为 TestScheduler
。当我这样做时,我终于能够看到 timer
observable 正在发出结果,而不是像以前那样 undefined
。
然而我又遇到了另一个问题。 timer
不会停止发出,所以我得到了一个像这样的长错误流:
Expected $.length = 26 to equal 2.
Unexpected $[2] = Object({ frame: 60, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[3] = Object({ frame: 90, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[4] = Object({ frame: 120, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[5] = Object({ frame: 150, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[6] = Object({ frame: 180, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[7] = Object({ frame: 210, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[8] = Object({ frame: 240, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
Unexpected $[9] = Object({ frame: 270, notification: Notification({ kind: 'N', value: LoadOrderLogisticStatusSuccess({ payload: Object({ 1047522: Object({ status: 0, partner: Object({ id: 1, slug: 'loggi' }), eta: '2020-06-09 10:00', pickupEta: '2020-06-09 12:00' }) }), type: 'load-order-logistic-status-success' }), error: undefined, hasValue: true }) }) in array.
前两个值没问题,其他的就不行。所以我做的第二件事是使用 takeUntil
运算符取消订阅 timer
一个测试完成(我不确定这是否是最好的方法,因为我绝不能使用这个操作员在测试之外,否则我的轮询将停止)。
这是代码的最终工作版本:
效果:
@Injectable()
export class OrderLogisticStatusEffects {
loadOrdersLogisticStatus$ = createEffect(() => ({ scheduler = asyncScheduler, stopTimer = EMPTY } = {}) =>
this.actions$.pipe(
ofType(LOAD_ORDERS_LOGISTIC_STATUS),
withLatestFrom(this.store$.pipe(select(orderLogisticsStatusPollingIntervalSelector))),
switchMap(([action, pollingInterval]) =>
timer(0, pollingInterval, scheduler).pipe(
takeUntil(stopTimer),
withLatestFrom(this.store$.pipe(select(selectedCompanySelector))),
switchMap(([timerNum, company]) => this.loadOrdersLogisticStatus(pollingInterval, company))
)
)
)
);
constructor(private actions$: Actions, private orderLogisticStatusService: OrderLogisticStatusService, private store$: Store<AppState>) {}
private loadOrdersLogisticStatus(
pollingInterval: number,
company: Company
): Observable<LoadOrderLogisticStatusSuccess | LoadOrderLogisticStatusFail> {
if (!company?.logisticsToken) {
return of(new LoadOrderLogisticStatusFail(new Error('No company selected')));
}
return this.orderLogisticStatusService.getOrdersStatus(company.logisticsToken).pipe(
timeout(pollingInterval),
map((result) => new LoadOrderLogisticStatusSuccess(result)),
catchError((error) => {
if (error.name === 'TimeoutError') {
console.warn('Timeout error while loadin logistic status service', error);
} else {
console.error('Error loading order logistic status', error);
Sentry.captureException(error);
}
return of(new LoadOrderLogisticStatusFail(error));
})
);
}
}
测试:
fdescribe('Order Logistic Status Effect', () => {
let actions$: Observable<Action>;
let effects: OrderLogisticStatusEffects;
describe('With a selected company', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: OrderLogisticStatusService, useValue: jasmine.createSpyObj('orderLogisticsStatusServiceSpy', ['getOrdersStatus']) },
OrderLogisticStatusEffects,
provideMockActions(() => actions$),
provideMockStore({
selectors: [
{
selector: orderLogisticsStatusPollingIntervalSelector,
value: 30,
},
{
selector: selectedCompanySelector,
value: {
logisticsToken: 'ey.xxxx.yyy',
},
},
],
}),
],
});
effects = TestBed.inject<OrderLogisticStatusEffects>(OrderLogisticStatusEffects);
});
it('should sucessfully load the orders logistics status', () => {
const service = TestBed.inject(OrderLogisticStatusService) as jasmine.SpyObj<OrderLogisticStatusService>;
service.getOrdersStatus.and.callFake(() => cold('a|', { a: mockData }));
actions$ = hot('a', { a: new LoadOrdersLogisticStatus() });
const expected = hot('a--b', {
a: new LoadOrderLogisticStatusSuccess(mockData),
b: new LoadOrderLogisticStatusSuccess(mockData),
});
const stopTimer = hot('----a', { a: 'stop' });
const testScheduler = getTestScheduler();
expect(effects.loadOrdersLogisticStatus$({ scheduler: testScheduler, stopTimer })).toBeObservable(expected);
});
});
});
const mockData = {
1047522: {
status: 0,
partner: {
id: 1,
slug: 'loggi',
},
eta: '2020-06-09 10:00',
pickupEta: '2020-06-09 12:00',
},
};