6

我在测试对我的效果的失败操作时遇到问题。

在这里给出一些上下文 loadProducts 效果是在调用Load操作时执行的。在 effect 内部执行 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,无论如何,我将 Load 操作创建为一个热的 observable,我的 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,
-     },
-   },
  ]

我知道错误是什么,但我不知道如何解决它。我知道弹珠测试世界

4

2 回答 2

8

我想解释一下为什么它一开始就不起作用。

如您所知,当您使用大理石图测试 observables 时,您使用的不是实时时间,而是虚拟时间。虚拟时间可以以frames. 框架的值可以变化(例如10, 1),但无论值如何,它都有助于说明您正在处理的情况。

例如,使用hot(--a---b-c),您描述了一个将发出以下值的可观察对象:aat 2ubat6ucat 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 */
]

hotcold,当被调用时,将分别实例化 ahotcoldobservable。他们的基类扩展了Observable类。

现在,看看当您处理内部可观察对象时会发生什么,就像您的示例中遇到的那样,这非常有趣:

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

responseobservable 是由于 订阅的,a这意味着错误通知将在frame of a+处发出original frame。也就是说,frame 1(a的到达) + frame1(当错误发出时) = frame 2

那么,为什么没有hot('-a')工作?

这是因为mergeMap处理事情的方式。当使用mergeMap和它的兄弟时,如果源已经完成但是操作符有内部的 observables 仍然是活动的(还没有完成),源的完成通知将不会被传递。只有当所有内部可观察对象也完成时。

另一方面,如果所有内部 observables 都完成了,但源没有完成,则没有完整的通知可以传递给链中的下一个订阅者。这就是它最初不起作用的原因

现在,让我们看看为什么它会这样工作:

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 个队列操作,则最旧的一个将优先。

从上面我们可以看出,source 会在内部 observable 发出错误之前发出一个完整的通知,这意味着当内部 observable 将发出捕获 error( outcome) 的值时,mergeMap将传递完整的通知。

最后,(b|)需要 incold('--(b|)', { b: outcome });因为catchError订阅 ,的 observableof(new productActions.LoadFail(error)))将在同一帧内发出并完成。当前帧保存当前选定动作的帧的值。在这种情况下,是2,来自{ frame: '2u', errorNotif: true, name: 'Load products fail' }

于 2020-05-07T17:21:47.903 回答
4

我找到了解决问题的方法,不确定是不是最好的方法,但基本上我添加了一个管道来完成 hot observable。请让我知道是否有任何其他解决方案。

 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);
});
于 2020-05-07T03:29:05.877 回答