Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing fail action - marble - ngrx Effects

I've got an issue testing a failed action on my effects.

To give a bit of context here loadProducts effect is executed when the Load action is called. Inside the effect an HTTP request is performed, in case this request is executed successfully the LoadSuccess action is called, otherwise LoadFail is called. Code here bellow

  @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)))
      ))
  );

To test this effect I used jest-marbles that is pretty much the same than jasmine-marbles, anyway, I created Load action as a hot observable, my http response as a cold and the default expected outcome.

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);
});

When I run the test throws an error saying my loadProducts observable and the expected outcome does not match.

  ✕ 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,
-     },
-   },
  ]

I know what the error is but I have no idea how to solve it. I am knew on the marbles testing world

like image 211
Denis Alejandro Salazar Rojas Avatar asked May 07 '20 02:05

Denis Alejandro Salazar Rojas


People also ask

How do I use marble testing with ngrx?

Here are some links to more information about marble testing with NgRx: In summary, we can use marble-diagram-like strings to describe an observable stream over time. This enables us to synchronously test asynchronous observable streams. There are two primary functions that we will be using: hot () creates a hot observable stream.

What happens to observables emitted by ngrx effects when an error occurs?

If we do that, the Observable that NgRx Effects subscribes to is going to be replaced by the Observable emitted in the catchError method, and then all future values emitted from action$ are going to be ignored (the new Observable in the error handler is not subscribed to them).

What is a success or failure action in ngrx?

If you've used NgRx before this may look extremely familiar. An action comes in which triggers something like an API call. This call will either succeed or fail and we dispatch a success or failure action as a result. In large NgRx codebases you might have this kind of effect all over the place.

Should you switch your effects testing to marble?

I would therefore highly recommend that if you don’t already do it, you should switch your Effects testing to the Marble tests. It turns out that catchError (and its predecessor, .catch () ), is a tricksy beast, making us think that it will just replace errors with new values, but actually switching entirely to a new Observable on the first error.


2 Answers

I'd like to explain why it didn't work in the first place.

As you know, when you're testing observables using marble diagrams, you're not using the real time, but a virtual time. Virtual time can be measured in frames. The value of a frame can vary(e.g 10, 1), but regardless of the value, it's something that helps illustrating the situation you're dealing with.

For example, with hot(--a---b-c), you describe an observable that will emit the following values: a at 2u, b at 6u and c at 8u(u - units of time).

Internally, RxJs creates a queue of actions and each action's task is to emit the value that it has been assigned. {n}u describes when the action will do its task.

For hot(--a---b-c), the action queue would look like this(roughly):

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

hot and cold, when called, will instantiate a hot and cold observable, respectively. Their base class extends the Observable class.

Now, it's very interesting to see what happens when you're dealing with inner observables, as encountered in your example:

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

The response observable is subscribed due to a, meaning that the error notification will be emitted at frame of a + original frame. That is, frame 1(a's arrival) + frame1(when the error is emitted) = frame 2.

So, why did hot('-a') not work ?

This is because of how mergeMap handles things. When using mergeMap and its siblings, if the source completes but the operator has inner observables that are still active(did not complete yet), the source's complete notification won't be passed along. It will be only when all the inner observables complete as well.

On the other hand, if all the inner observables complete, but the source didn't, there is no complete notification to be passed along to the next subscriber in the chain. This is why it hadn't worked initially.

Now, let's see why it does work this way:

actions$ = hot('-a|', { a: action});

const response = cold('-#|)', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);

const expected = cold('--(b|)', { b: outcome });

the action's queue would now look like this:

queue = [
  { frame: '1u', value: 'a' },
  { frame: '2u', completeNotif: true },
]

When a is received, the response will be subscribed and because it's an observable created with cold(), its notifications will have to be assigned to actions and put in the queue accordingly.

After response has been subscribed to, the queue would look like this:

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 '-#|'
]

Notice that if 2 queue actions should be emitted at the same frame, to oldest one will take precedence.

From the above, we can tell that the source will emit a complete notification before the inner observable emits the error, meaning that when the inner observable will emit the value resulted from catching the error(outcome), the mergeMap will pass along the complete notification.

Finally, (b|) is needed in cold('--(b|)', { b: outcome }); because the observable which catchError subscribes to, of(new productActions.LoadFail(error))), will emit and complete within the same frame. The current frame holds the value of the current selected action's frame. In this case, is 2, from { frame: '2u', errorNotif: true, name: 'Load products fail' }.

like image 104
Andrei Gătej Avatar answered Oct 17 '22 21:10

Andrei Gătej


I found a way to solve my issue, not sure is the best way to do it, but basically I added a pipe to complete the hot observable. Please let me know if there is any other solution.

 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);
});
like image 40
Denis Alejandro Salazar Rojas Avatar answered Oct 17 '22 22:10

Denis Alejandro Salazar Rojas