测试失败操作 - 大理石 - 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 来衡量。帧的值可能会有所不同(例如 101),但无论值如何,它都有助于说明您正在处理的情况。

例如,使用 hot(--a---b-c),您描述了一个将发出以下值的可观察对象:a2ub6uc8uu - 时间单位)。

在内部,RxJs 创建了一个动作队列,每个动作的任务是发出分配给它的值。 {n}u 描述操作何时执行其任务。

对于hot(--a---b-c)动作队列看起来像这样(大致):

queue = [
  { frame: '2u', value: 'a' }/* aAction */, 
  { frame: '6u', value: 'b' }/* bAction */, 
  { frame: '8u', value: 'c' }/* cAction */
]

hotcold 在调用时将分别实例化 hotcold 可观察对象。他们的基数 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' }.