Search code examples
angularunit-testingjasminengxs

How to test that an Action was dispatched from within another Action in NGXS?


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


Solution

  • 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.