Search code examples
javascripttypescriptreduxredux-sagaredux-saga-test-plan

redux-saga-test-plan expectSaga- Testing reducer with state branch


I am having a bit of trouble properly testing my saga. The issue stems from the fact that when running the saga, the reducer is mounted at state: {...initialState} whereas my saga select effects are expecting the reducer mounted at state: {authentication: {...initialState}}

Due to this I am unable to fully test the reducer/saga combo, since the shape of the final state object is inconsistent with the actual shape of the store.

The saga being tested:

export default function* rootAuthenticationSaga() {
  while (true) {
      refreshToken = yield select((state: ApplicationRootState) => state.authentication.refreshToken);
      ... more code here
}

One of my test is as follows:

    test('logs the user in if they provide a valid email and password', () => {
      const mockRefreshPoller = createMockTask();
      const initialState = {
        accessToken: '',
        refreshToken: '',
        userId: '',
      }
      return expectSaga(rootAuthenticationSaga)
        .withReducer(authenticationReducer, initialState)
        .provide([
          // some mock providers set up here
        ])
        // ...more asserts here
        .put(authenticationActions.saveTokens({accessToken: 'VALID_ACCESS_TOKEN', refreshToken: 'VALID_REFRESH_TOKEN'}))
        .hasFinalState({
           accessToken: 'VALID_ACCESS_TOKEN',
           refreshToken: 'VALID_REFRESH_TOKEN',
           userId: 'USER_ID',
        })
        .dispatch(authenticationActions.login.request({email: 'VALID_EMAIL', password: 'VALID_PASSWORD'}))
        .run()
    });

In the above test, the select() fails as the correct path (with injection via withReducer) is at state.refreshToken not state.authentication.refreshToken

If I inject the state via withState({authentication: {refreshToken, ...}}) then the select works as expected, but all the reducer actions happen against the state root, with my final state having the (incorrect) shape:

{state: 
  authentication: {
    refreshToken: '',
    ...
  },
  refreshToken: 'VALID_REFRESH_TOKEN',
  ...
}

Solution

  • The trick to this was to "mount" the reducer in question at the right "branch" by creating a

    const reducerUnderTest = createReducer({
      authentication: authenticationReducer,
    })
    

    and passing that into the withReducer(reducerUnderTest, {authentication: {...initialState}} with the correct shape state.