NGXS: How to test if an action was dispatched?
Asked Answered
S

3

12

How to unit test whether an action was dispatched?

For example, in a LogoutService, I have this simple method:

  logout(username: string) {
    store.dispatch([new ResetStateAction(), new LogoutAction(username)]);
  }

I need to write a unit test that verifies that the two actions are dispatched:

  it('should dispatch ResetState and Logout actions', function () {
    logoutService.logout();

    // how to check the dispactched actions and their parameters?
    // expect(...)
  });

How can I check the dispatched actions?

Seville answered 28/6, 2018 at 17:19 Comment(3)
create a mock of the store and check if dispatched was called once with the correspondng arguments.Kimmie
I haven't tried it in my testing with NGXS yet, but could you subscribe to the action stream to be notified when those actions are dispatched?Pooka
I have done what @GarthMason has mentioned and it works. Thanks!Resemblance
S
18

NGXS Pipeable Operators

Actions in NGXS are handled with Observables. NGXS provides you Pipeable Operators, for your test you could use the ofActionDispatched. Here is the list I have taken from the NGXS documentation:

  • ofAction triggers when any of the below lifecycle events happen
  • ofActionDispatched triggers when an action has been dispatched
  • ofActionSuccessful triggers when an action has been completed successfully
  • ofActionCanceled triggers when an action has been canceled
  • ofActionErrored triggers when an action has caused an error to be thrown
  • ofActionCompleted triggers when an action has been completed whether it was successful or not (returns completion summary)

Answer

1. Create variable actions$

describe('control-center.state', () => {
  let actions$: Observable<any>;

  // ...
});

2. Initialize variable actions$ with observable

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [
      NgxsModule.forRoot([AppState]),
      NgxsModule.forFeature([ControlCenterState])
    ]
  });
  store = TestBed.get(Store);
  actions$ = TestBed.get(Actions);
})

3.1 Test if 1 action has been called:

Filter your actions from the stream with the operator ofActionsDispatched().

it('should dispatch LogoutAction', (done) => {
  actions$.pipe(ofActionDispatched(LogoutAction)).subscribe((_) => {
    done();
  });

  service.logout();
});

3.2 Test if multiple actions have been called:

Use the RXJS zip operator to combine the two observables with the ofActionsDispatched() function (zip: after all observables emit, emit values as an array).

it('should dispatch ResetStateAction and LogoutAction', (done) => {
  zip(
    actions$.pipe(ofActionDispatched(ResetStateAction)),
    actions$.pipe(ofActionDispatched(LogoutAction))
  ).subscribe((_) => {
    done();
  });

  service.logout();
});

The spec will not complete until its done is called. If done is not called a timeout exception will be thrown.

From the Jasmine documentation.

Sneaky answered 2/8, 2018 at 8:50 Comment(2)
I have tested this implementation and there is a small error in the ofActionDispatched filter, it should be called like this: ofActionDispatched(ResetStateAction, LogoutAction). Note that the actions should not be wrapped in a list. Additionally, the done function should not be needed. If implemented like above the test will actually falsely pass. Wrapping the test in async works as expected.Buckhound
@Buckhound ofActionDispatched takes indeed a list, changed it. About your next statement, you need the done function otherwise it will falsely pass: If the done function is passed, the test expects it to be called, if it is not called in x amount of seconds the test will fail.Sneaky
M
1

Using Jasmine Spies

I believe that in unit testing the actual implementation of all the related dependencies should be mocked and hence we should not be including any actual stores in here. Here we are providing a jasmine spy for Store and just checking whether certain actions are dispatched with correct parameters. This could also be used to provide stub data too.

describe('LogoutService', () => {
  let storeSpy: jasmine.SpyObj<Store>;

  beforeEach(() => {
    storeSpy = jasmine.createSpyObj(['dispatch']);

    TestBed.configureTestingModule({
      providers: [LogoutService, { provide: Store, useValue: storeSpy }]
    });
  })

  it('should dispatch Logout and Reset actions', () => {
    storeSpy.dispatch.withArgs([
      jasmine.any(ResetStateAction), 
      jasmine.any(LogoutAction)])
     .and
     .callFake(([resetAction, logoutAction]) => {
       expect(resetAction.payload).toEqual({...something});
       expect(logoutAction.payload).toEqual({...somethingElse});
    });

    TestBed.inject(LogoutService).logout();
});
Matriarch answered 9/11, 2020 at 5:58 Comment(0)
M
0

I tried this approach to test if both actions were called:

3. Test if actions are being called

// ...
it('should call actions ResetStateAction and LogoutAction', async( () => {
  let actionDispatched = false;
  zip(
    actions$.pipe(ofActionDispatched(ResetStateAction)),
    actions$.pipe(ofActionDispatched(LogoutAction))
  )
  .subscribe( () => actionDispatched = true );

  store.dispatch([new ResetStateAction(), new LogoutAction()])
    .subscribe(
      () => expect(actionDispatched).toBe(true)
    );
}));
// ...
Martian answered 22/2, 2019 at 17:11 Comment(4)
I don't understand the significance of your test. You dispatch 2 actions to then test if they were dispatched.Tracietracing
@Tracietracing the dispatch calls in my test are just an example. In a real-life scenario you would test that a method or a third different action effectively calls the actions of interest during execution.Martian
@ClaudioSuardi You are talking about the ofActionDispatched that it "seems to be equal to ResetStateAction OR LogoutAction". This is incorrect, documentation: This will ONLY grab actions that have just been dispatched, it filters out only the ones you pass.Sneaky
@Sneaky I get what you're saying. I changed my answer to avoid leaving out incorrect information.Martian

© 2022 - 2024 — McMap. All rights reserved.