Skip to content
This repository has been archived by the owner on May 27, 2020. It is now read-only.

Mock dispatch #1

Open
splincode opened this issue May 28, 2019 · 2 comments
Open

Mock dispatch #1

splincode opened this issue May 28, 2019 · 2 comments

Comments

@splincode
Copy link
Member

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?

@aymeric-duchein
Copy link
Contributor

Hi!
I think I may have found a solution.

tl;dr this solution uses internals api of ngxs that might change (maybe? I don't know). Use at your own risk 😉

Starting from @uiii 's solution, I tried to create an helper function allowing me to test this easily.

  @Action(SetEntity)
  setEntity({ patchState, dispatch }: StateContext<ConfigurationStateModel>, { entity }: SetEntity) {
    patchState({
      id: entity.id
    });

    dispatch(new AfterEntity());
  }

So... after a fun afternoon I came up with this:

const mockNGXSState: <T>(stateClass: any) => StateContext<T> = <T>(stateClass: any) => {
  const contextFactory = TestBed.get(NGXS_STATE_CONTEXT_FACTORY);
  const mockStateCtx = createMockStateContext<T>(stateClass);
  jest.spyOn(contextFactory, 'createStateContext').mockReturnValue(mockStateCtx);

  return mockStateCtx;
};

const createMockStateContext:  <T>(stateClass: any) => StateContext<T> = <T>(stateClass: any) => {
  const { defaults, name } = stateClass['NGXS_OPTIONS_META'];
  const store: Store = TestBed.get(Store);

  return {
    getState: jest.fn().mockImplementation(() => defaults),
    setState: jest.fn().mockImplementation((val: T) => {
      store.reset({ [name]: val });
    }),
    patchState: jest.fn().mockImplementation((val: Partial<T>) => {
      store.reset({ [name]: { ...defaults, ...val } });
    }),
    dispatch: jest.fn().mockImplementation(() => of())
  };
};

this allowed me to do this:

describe('Configuration State', () => {
  let store: Store;
  let mockStateSpies;
  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [NgxsModule.forRoot([MyState])]
    });
    mockStateSpies = mockNGXSState(MyState);

    store = TestBed.get(Store);


    store.reset({
      configuration: initialConfigurationState
    });
  });
 test('should set Entity ', () => {
    const entity = {
      id: '42',
      label: 'foo'
    };

    store.dispatch(new SetEntity(entity));

    const entityId = store.selectSnapshot(MyState.getId);
    expect(entityId).toEqual(entity.id);
    expect(mockStateSpies.dispatch).toHaveBeenCalledWith(new AfterEntity());
  });
});

I think this solution can easily be used and don't use to much from the internals of the lib. The only non-public parts used are the createStateContext function from the NGXS_STATE_CONTEXT_FACTORY and the 'NGXS_OPTIONS_META' metadata property.

I used Jest here but the mocks functions could easily be replaced with jasmine ones or something else.

What do you think of this solution? Could this help you providing some testing helpers?

@splincode
Copy link
Member Author

PR's please

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants