question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

🚀[FEATURE]: How to mock dispatch

See original GitHub issue

Hi, I’m creating unit tests and have a problem when testing actions. I would like to mock dispatch function in a way it really dispatches only a tested action and any additional dispatch call are only spied.

Something like this:

    it('should login with valid credentials', async (done) => {
        const action = new Login(validCredentials)

        const dispatchSpy = spyOn(store, 'dispatch').withArgs(action).and.callThrough()

        store.dispatch(action)

        await expect(store.selectSnapshot(AuthState.getError)).toBeFalsy()
        await expect(store.selectSnapshot(AuthState.getPending)).toBeTruthy()

        authTrigger.next()

        const dispatchedLoginSuccessAction = [].concat(...dispatchSpy.calls.allArgs()).find(a => a instanceof LoginSuccess)

        await expect(dispatchedLoginSuccessAction).toBeTruthy()
        await expect(dispatchedLoginSuccessAction.payload).toEqual(token)
    })

BUT! This doesn’t work, because as I investigate, the store.dispatch is different from the dispatch function in the action context. I know I can use the construction like this without mocking (and it works):

        actions$.pipe(ofActionDispatched(LoginSuccess)).subscribe(async (action) => {
            await expect(action).toBeTruthy()
            done()
        })

BUT! I don’t want to actually dispatch additional actions because of side effects. Consider the tested action dispatches an action from another module, so I would have to mock all services which causes side effects in that module.

I’ve found out the actual dispatch to be mocked is the one in the InternalStateOperations object, but I don’t know how to mock it.

QUESTION So what is the proper way to make tests like this?

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:3
  • Comments:13 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
markwhitfeldcommented, Jun 4, 2021

Here is a code snippet of a utility that I like to use for capturing actions that have been dispatched in my tests. Hope this helps!

You can either add it to your imports (NgxsActionCollector.collectActions()) to start collecting actions from NGXS initialisation. Or you can just inject it from the TestBed and call start(), stop() and reset() as needed.

Example usage in the doc comments in the code.

import {
  Injectable,
  ModuleWithProviders,
  NgModule,
  OnDestroy,
} from '@angular/core';
import { Actions } from '@ngxs/store';
import { ActionStatus } from '@ngxs/store/src/actions-stream';
import { ReplaySubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class NgxsActionCollector implements OnDestroy {
  /**
   * Including this in your imported modules will
   * set up the the action collector to start collecting actions
   * from before Ngxs initializes
   * @example
   * // In your module declaration for your tests:
   * {
   *   imports: [
   *     NgxsActionCollector.collectActions(),
   *     NgxsModule.forRoot([MyState]),
   *   ],
   *   // ...
   * }
   * // and then in your test:
   * const actionCollector = TestBed.inject(NgxsActionCollector);
   * const actionsDispatched = actionCollector.dispatched;
   * const action = actionsDispatched.find(
   *   (item) => item instanceof MyAction
   * );
   * expect(action).toBeDefined();
   * @returns A module that starts the collector immediately
   */
  public static collectActions(): ModuleWithProviders<any> {
    @NgModule()
    class NgxsActionCollectorModule {
      constructor(collectorService: NgxsActionCollector) {
        collectorService.start();
      }
    }
    return {
      ngModule: NgxsActionCollectorModule,
      providers: [Actions, NgxsActionCollector],
    };
  }

  private destroyed$ = new ReplaySubject<void>(1);
  private stopped$ = new Subject<void>();
  private started = false;

  public readonly dispatched: any[] = [];
  public readonly completed: any[] = [];
  public readonly successful: any[] = [];
  public readonly errored: any[] = [];
  public readonly cancelled: any[] = [];

  constructor(private actions$: Actions) {}

  start() {
    if (this.started) {
      return;
    }
    this.started = true;
    this.actions$
      .pipe(takeUntil(this.destroyed$), takeUntil(this.stopped$))
      .subscribe({
        next: (actionCtx: { status: ActionStatus; action: any }) => {
          switch (actionCtx?.status) {
            case ActionStatus.Dispatched:
              this.dispatched.push(actionCtx.action);
              break;
            case ActionStatus.Successful:
              this.successful.push(actionCtx.action);
              this.completed.push(actionCtx.action);
              break;
            case ActionStatus.Errored:
              this.errored.push(actionCtx.action);
              this.completed.push(actionCtx.action);
              break;
            case ActionStatus.Canceled:
              this.cancelled.push(actionCtx.action);
              this.completed.push(actionCtx.action);
              break;
            default:
              break;
          }
        },
        complete: () => {
          this.started = false;
        },
        error: () => {
          this.started = false;
        },
      });
  }

  reset() {
    function clearArray(arr) {
      arr.splice(0, arr.length);
    }
    clearArray(this.dispatched);
    clearArray(this.completed);
    clearArray(this.successful);
    clearArray(this.errored);
    clearArray(this.cancelled);
  }

  stop() {
    this.stopped$.next();
  }

  ngOnDestroy(): void {
    this.destroyed$.next();
  }
}
1reaction
DanBoSlicecommented, Jun 2, 2021

As much as I love using NGXS, this seems to be a major downside. The catch of using redux state management should be the simplicity of testing. However, not being able to test dispatched actions from another action in a straight-forward way goes directly against this advantage.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to mock dispatch with jest.fn() - Stack Overflow
Here's an example that worked for me (2022):. (Navbar.test.js) import store from "../../store/redux-store"; import { useDispatch, ...
Read more >
Unit Testing Redux Thunks with a Mock Dispatch Function
For our first test, the first thing that we need to do is type const dispatch = jest.fn. Below that, we're going to...
Read more >
Redux DevTools: Tips and tricks for faster debugging
We can add our actions in dispatcher and it works just like action dispatched via Redux API. This kind of mocking helps in...
Read more >
A guide to module mocking with Jest - Emma Goto
Mocking a named import · Mocking only the named import (and leaving other imports unmocked) · Mocking a default import · Mocking default...
Read more >
Modern React Redux Toolkit - Login & User Registration ...
Everything in toolkit is grouped as Features. it's called duck pattern. ... 4import { useSelector, useDispatch } from 'react-redux';.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found