1

I have been using NGXS widely across an application but there some things that I still can't do in a proper way and the documentation or other questions asked here haven't helped so far. One of such things is regarding the testing of actions, specifically testing that an action was called from within another action. Check the following:

store.ts

@Action(ActionToBeTested)
  public doActionToBeTested(
    ctx: StateContext<MyState>,
  ): void {
    const state = ctx.getState();

    // update state in some way

    ctx.patchState(state);

    this.restApiService.save(state.businessData)
      .pipe(
        mergeMap(() =>
          this.store.dispatch(new SomeOtherAction(state.businessData)),
        )
      )
      .subscribe();
  }

store.spec.ts

it('should test the state was update, restapi was called and action was dispatched', async(() => {
    store.reset({...someMockState});

    store.dispatch(new ActionToBeTested());

    store
      .selectOnce((state) => state.businessData)
      .subscribe((data) => {
        expect(data).toBe('Something I expect');
        expect(restApiServiceSpy.save).toHaveBeenCalledTimes(1);
        
      });
  }));

With this I can test that the state was updated and the rest service was called but I simply cannot test that the action SomeOtheAction was dispatched. I have tried a lot of things already since trying to spy on the store after dispatching the first action (kind of crazy I know) to what is described in this answer but with no success.

Has anyone came across similar difficulties and found a solution. If so, please, point me in the correct direction and I'll be happy to walk the path myself. Thank you

António Quadrado
  • 1,307
  • 2
  • 17
  • 34
  • Well, I would focus on the final state rather than which actions are dispatched. – Eldar Nov 11 '20 at 14:52
  • Yeah @Eldar, I tried that as well but if I check the state related to other action it's not what I would expect, it's like the action is not actually called during the test although it is when the application is running. – António Quadrado Nov 11 '20 at 15:19
  • I don't have any experience with ngsx but checked out their unit testing documentation and noticed two things: They encourage to use `store.selectSnapshot` instead of `selectOnce` and if an action is async they return the observable from the action then await it while testing. – Eldar Nov 11 '20 at 15:31
  • Even If I change the approach to match what you just described it doesn't solve the problem, the test updated state still doesn't reflect the calling the of the second action. I actually think the action is not being called in the test context. – António Quadrado Nov 11 '20 at 15:56
  • Does your mock service return an `Observable`? – Eldar Nov 11 '20 at 16:18
  • Yes, it does. Actually, I have arrived to a solution. I will post an answer, feel free to share your thoughts on that as well and thank you for your inputs. – António Quadrado Nov 11 '20 at 16:52
  • So the way this is coded, might be adding to the difficulty of writing a unit test. Is there a reason you dispatch an action from mergeMap instead of the subscribe function? – Austin Born Aug 23 '23 at 13:42

1 Answers1

1

After dwindling around the problem for almost a whole day I arrived at something that can be considered a solution. It's the following:

it('should test the state was update, restapi was called and action was dispatched', (done) => {
      store.reset({...someMockState});
      
      actions$.pipe(ofActionSuccessful(DeleteSource)).subscribe((_) => {
        const updatedData = store.selectSnapshot(
          (state) => state.businessData,
        );
        
        expect(updatedData).toBe('Something I expect due to the first action');
        expect(updatedData).toBe('Something I expect due to the SECOND action');
        expect(restApiServiceSpy.save).toHaveBeenCalledTimes(1);
        done();
      });
  
      store.dispatch(new ActionToBeTested());
});

So, what's happening here? As suggested by @Eldar in the question's comments I decided to test the final state instead of checking if the action was actually called. In order for that to work I had to inject the actions' stream as described here and 'listen' to the 'action success' event using the operator ofActionSuccessful. This allows for checking the state only after the completion of the action instead of it being just dispatched so I could test that everything was as supposed in the final state.

Few things to remember:

  • Don't forget to call the done function after you completed your expectations otherwise you'll get a timeout error.
  • done only works if the callback you pass to the it() function is not wrapped with the async function.
  • Make sure that your mocks return something, even if it's undefined. At first I had my mock service method return of() instead of of(undefined) and that was causing problems in some cases.
  • Also, you can define your mocks with a defined delay like so, of(undefined).delay(1), so you are able to control the timings of your calls and expectations.

Hope this can be helpful to someone.

António Quadrado
  • 1,307
  • 2
  • 17
  • 34