测试失败操作 - 大理石 - ngrx 效果
Testing fail action - marble - ngrx Effects
我在测试对我的效果的失败操作时遇到了问题。
为了在此处提供一些上下文,loadProducts 效果在调用 Load 操作时执行。在执行 HTTP 请求的效果中,如果此请求成功执行,则调用 LoadSuccess 操作,否则调用 LoadFail。代码如下
@Effect()
loadProducts$ = this.actions$.pipe(
ofType(productActions.ProductActionTypes.Load),
mergeMap((action: productActions.Load) =>
this.productService.getProducts().pipe(
map((products: Product[]) => (new productActions.LoadSuccess(products))),
catchError(error => of(new productActions.LoadFail(error)))
))
);
为了测试这种效果,我使用了与 jasmine-marbles 几乎相同的 jest-marbles,无论如何,我将加载操作创建为热可观察对象,将我的 http 响应创建为冷响应和默认预期结果。
it('should return a LoadFail action, with an error, on failure', () => {
const action = new Load();
const errorMessage = 'Load products fail';
const outcome = new LoadFail(errorMessage);
actions$ = hot('-a', { a: action});
const response = cold('-#|', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
expect(effects.loadProducts$).toBeObservable(expected);
});
当我 运行 时,测试抛出一个错误,指出我的 loadProducts 可观察结果与预期结果不匹配。
✕ should return a LoadFail action, with an error, on failure (552ms)
Product effects › loadProducts › should return a LoadFail action, with an error, on failure
expect(received).toBeNotifications(expected)
Expected notifications to be:
[{"frame": 20, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": "Load products fail", "type": "[Product] Load Fail"}}}, {"frame": 20, "notification": {"error": undefined, "hasValue": false, "kind": "C", "value": undefined}}]
But got:
[{"frame": 20, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": "Load products fail", "type": "[Product] Load Fail"}}}]
Difference:
- Expected
+ Received
Array [
Object {
"frame": 20,
"notification": Notification {
"error": undefined,
"hasValue": true,
"kind": "N",
"value": LoadFail {
"payload": "Load products fail",
"type": "[Product] Load Fail",
},
},
},
- Object {
- "frame": 20,
- "notification": Notification {
- "error": undefined,
- "hasValue": false,
- "kind": "C",
- "value": undefined,
- },
- },
]
我知道错误是什么,但我不知道如何解决。我在弹珠测试界广为人知
我找到了解决问题的方法,不确定是不是最好的方法,但基本上我添加了一个管道来完成热观察。如果有任何其他解决方案,请告诉我。
it('should return a LoadFail action, with an error, on failure', () => {
const action = new Load();
const errorMessage = 'Load products fail';
const outcome = new LoadFail(errorMessage);
actions$ = hot('-a|', { a: action});
const response = cold('-#|)', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
expect(effects.loadProducts$).toBeObservable(expected);
});
我想先解释一下为什么它不起作用。
如您所知,当您使用弹珠图测试可观察对象时,您使用的不是实时,而是虚拟时间[=101] =].虚拟时间可以用 frames
来衡量。帧的值可能会有所不同(例如 10
、1
),但无论值如何,它都有助于说明您正在处理的情况。
例如,使用 hot(--a---b-c)
,您描述了一个将发出以下值的可观察对象:a
在 2u
、b
在 6u
和c
在 8u
(u
- 时间单位)。
在内部,RxJs 创建了一个动作队列,每个动作的任务是发出分配给它的值。 {n}u
描述操作何时执行其任务。
对于hot(--a---b-c)
,动作队列看起来像这样(大致):
queue = [
{ frame: '2u', value: 'a' }/* aAction */,
{ frame: '6u', value: 'b' }/* bAction */,
{ frame: '8u', value: 'c' }/* cAction */
]
hot
和 cold
在调用时将分别实例化 hot
和 cold
可观察对象。他们的基数 class 扩展了 Observable
class.
现在,看看当您处理内部可观察对象时会发生什么,就像您的示例中遇到的那样:
actions$ = hot('-a', { a: action}); // 'a' - emitted at frame 1
const response = cold('-#|', {}, errorMessage); // Error emitted at 1u after it has been subscribed
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome }); // `b` and `complete` notification, both at frame 2
response
observable 由于 a
而被订阅,这意味着错误通知将在 frame of a
+ original frame
时发出。即frame 1
(a
的到来)+frame1
(报错时)=frame 2
.
那么,为什么 hot('-a')
不起作用?
这是因为 mergeMap
处理事情的方式。使用 mergeMap
及其兄弟时,如果源已完成但运算符的内部可观察对象仍处于活动状态(尚未完成),则源的完成通知将不会过去了。只有当所有内部可观察对象也完成时才会这样做。
另一方面,如果所有内部可观察对象都完成,但源没有完成,则没有完整的通知传递给链中的下一个订阅者。 这就是它最初不起作用的原因。
现在,让我们看看为什么它会这样工作:
actions$ = hot('-a|', { a: action});
const response = cold('-#|)', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
操作的队列现在看起来像这样:
queue = [
{ frame: '1u', value: 'a' },
{ frame: '2u', completeNotif: true },
]
收到 a
时,response
将被订阅,因为它是使用 cold()
创建的可观察对象,其 通知 将具有分配给动作并相应地放入队列中。
订阅 response
后,队列将如下所示:
queue = [
// `{ frame: '1u', value: 'a' },` is missing because when an action's task is done
// the action itself is removed from the queue
{ frame: '2u', completeNotif: true }, // Still here because the first action didn't finish
{ frame: '2u', errorNotif: true, name: 'Load products fail' }, // ' from '-#|'
{ frame: '3u', completeNotif: true },// `|` from '-#|'
]
请注意,如果应在同一帧发出 2 个队列动作,则最旧的动作优先。
从上面我们可以看出,源将在内部可观察对象发出错误之前发出一个完整的通知,这意味着当内部可观察对象发出由以下结果产生的值时捕获错误(outcome
),mergeMap
将传递完整的通知。
最后,cold('--(b|)', { b: outcome });
中需要 (b|)
,因为 catchError
订阅的可观察对象 of(new productActions.LoadFail(error)))
将在同一帧内发出和完成。当前帧保存当前选定动作的帧的值。在这种情况下,是 2
,来自 { frame: '2u', errorNotif: true, name: 'Load products fail' }
.
我在测试对我的效果的失败操作时遇到了问题。
为了在此处提供一些上下文,loadProducts 效果在调用 Load 操作时执行。在执行 HTTP 请求的效果中,如果此请求成功执行,则调用 LoadSuccess 操作,否则调用 LoadFail。代码如下
@Effect()
loadProducts$ = this.actions$.pipe(
ofType(productActions.ProductActionTypes.Load),
mergeMap((action: productActions.Load) =>
this.productService.getProducts().pipe(
map((products: Product[]) => (new productActions.LoadSuccess(products))),
catchError(error => of(new productActions.LoadFail(error)))
))
);
为了测试这种效果,我使用了与 jasmine-marbles 几乎相同的 jest-marbles,无论如何,我将加载操作创建为热可观察对象,将我的 http 响应创建为冷响应和默认预期结果。
it('should return a LoadFail action, with an error, on failure', () => {
const action = new Load();
const errorMessage = 'Load products fail';
const outcome = new LoadFail(errorMessage);
actions$ = hot('-a', { a: action});
const response = cold('-#|', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
expect(effects.loadProducts$).toBeObservable(expected);
});
当我 运行 时,测试抛出一个错误,指出我的 loadProducts 可观察结果与预期结果不匹配。
✕ should return a LoadFail action, with an error, on failure (552ms)
Product effects › loadProducts › should return a LoadFail action, with an error, on failure
expect(received).toBeNotifications(expected)
Expected notifications to be:
[{"frame": 20, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": "Load products fail", "type": "[Product] Load Fail"}}}, {"frame": 20, "notification": {"error": undefined, "hasValue": false, "kind": "C", "value": undefined}}]
But got:
[{"frame": 20, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": "Load products fail", "type": "[Product] Load Fail"}}}]
Difference:
- Expected
+ Received
Array [
Object {
"frame": 20,
"notification": Notification {
"error": undefined,
"hasValue": true,
"kind": "N",
"value": LoadFail {
"payload": "Load products fail",
"type": "[Product] Load Fail",
},
},
},
- Object {
- "frame": 20,
- "notification": Notification {
- "error": undefined,
- "hasValue": false,
- "kind": "C",
- "value": undefined,
- },
- },
]
我知道错误是什么,但我不知道如何解决。我在弹珠测试界广为人知
我找到了解决问题的方法,不确定是不是最好的方法,但基本上我添加了一个管道来完成热观察。如果有任何其他解决方案,请告诉我。
it('should return a LoadFail action, with an error, on failure', () => {
const action = new Load();
const errorMessage = 'Load products fail';
const outcome = new LoadFail(errorMessage);
actions$ = hot('-a|', { a: action});
const response = cold('-#|)', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
expect(effects.loadProducts$).toBeObservable(expected);
});
我想先解释一下为什么它不起作用。
如您所知,当您使用弹珠图测试可观察对象时,您使用的不是实时,而是虚拟时间[=101] =].虚拟时间可以用 frames
来衡量。帧的值可能会有所不同(例如 10
、1
),但无论值如何,它都有助于说明您正在处理的情况。
例如,使用 hot(--a---b-c)
,您描述了一个将发出以下值的可观察对象:a
在 2u
、b
在 6u
和c
在 8u
(u
- 时间单位)。
在内部,RxJs 创建了一个动作队列,每个动作的任务是发出分配给它的值。 {n}u
描述操作何时执行其任务。
对于hot(--a---b-c)
,动作队列看起来像这样(大致):
queue = [
{ frame: '2u', value: 'a' }/* aAction */,
{ frame: '6u', value: 'b' }/* bAction */,
{ frame: '8u', value: 'c' }/* cAction */
]
hot
和 cold
在调用时将分别实例化 hot
和 cold
可观察对象。他们的基数 class 扩展了 Observable
class.
现在,看看当您处理内部可观察对象时会发生什么,就像您的示例中遇到的那样:
actions$ = hot('-a', { a: action}); // 'a' - emitted at frame 1
const response = cold('-#|', {}, errorMessage); // Error emitted at 1u after it has been subscribed
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome }); // `b` and `complete` notification, both at frame 2
response
observable 由于 a
而被订阅,这意味着错误通知将在 frame of a
+ original frame
时发出。即frame 1
(a
的到来)+frame1
(报错时)=frame 2
.
那么,为什么 hot('-a')
不起作用?
这是因为 mergeMap
处理事情的方式。使用 mergeMap
及其兄弟时,如果源已完成但运算符的内部可观察对象仍处于活动状态(尚未完成),则源的完成通知将不会过去了。只有当所有内部可观察对象也完成时才会这样做。
另一方面,如果所有内部可观察对象都完成,但源没有完成,则没有完整的通知传递给链中的下一个订阅者。 这就是它最初不起作用的原因。
现在,让我们看看为什么它会这样工作:
actions$ = hot('-a|', { a: action});
const response = cold('-#|)', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
操作的队列现在看起来像这样:
queue = [
{ frame: '1u', value: 'a' },
{ frame: '2u', completeNotif: true },
]
收到 a
时,response
将被订阅,因为它是使用 cold()
创建的可观察对象,其 通知 将具有分配给动作并相应地放入队列中。
订阅 response
后,队列将如下所示:
queue = [
// `{ frame: '1u', value: 'a' },` is missing because when an action's task is done
// the action itself is removed from the queue
{ frame: '2u', completeNotif: true }, // Still here because the first action didn't finish
{ frame: '2u', errorNotif: true, name: 'Load products fail' }, // ' from '-#|'
{ frame: '3u', completeNotif: true },// `|` from '-#|'
]
请注意,如果应在同一帧发出 2 个队列动作,则最旧的动作优先。
从上面我们可以看出,源将在内部可观察对象发出错误之前发出一个完整的通知,这意味着当内部可观察对象发出由以下结果产生的值时捕获错误(outcome
),mergeMap
将传递完整的通知。
最后,cold('--(b|)', { b: outcome });
中需要 (b|)
,因为 catchError
订阅的可观察对象 of(new productActions.LoadFail(error)))
将在同一帧内发出和完成。当前帧保存当前选定动作的帧的值。在这种情况下,是 2
,来自 { frame: '2u', errorNotif: true, name: 'Load products fail' }
.