reduxredux-thunknockredux-mock-store

Why does redux-mock-store don't show an action dispatched in catch promises?


I'm very bad when it comes to thinking of a title question, sorry for that.

My Problem:

I'm unit testing my async redux actions like it's suggested in the docs. I mock the API calls with nock and check for the dispatched actions with redux-mock-store. It works great so far, but I have one test that fails even though it clearly does work. The dispatched action neither does show up in the array returned by store.getActions() nor is the state changed in store.getState(). I'm sure that it does happen because I can see it when I test manually and observe it with Redux Dev Tools.

The only thing that is different in this action dispatch is that it is called in a promise in a catch of another promise. (I know that sounds confusing, just look at the code!)

What my code looks like:

The action:

export const login = (email, password) => {
    return dispatch => {
        dispatch(requestSession());
        return httpPost(sessionUrl, {
            session: {
                email,
                password
            }
        })
        .then(data => {
            dispatch(setUser(data.user));
            dispatch(push('/admin'));
        })
        .catch(error => {
            error.response.json()
            .then(data => {
                dispatch(setError(data.error))
            })
        });
    };
}

This httpPost method is just a wrapper around fetch that throws if the status code is not in the 200-299 range and already parses the json to an object if it doesn't fail. I can add it here if it seems relevant, but I don't want to make this longer then it already is.

The action that doesn't show up is dispatch(setError(data.error)).

The test:

it('should create a SET_SESSION_ERROR action', () => {
    nock(/example\.com/)
    .post(sessionPath, {
        session: {
            email: fakeUser.email,
            password: ''
        }
    })
    .reply(422, {
        error: "Invalid email or password"
    })

    const store = mockStore({
        session: {
            isFetching: false,
            user: null,
            error: null
        }
    });

    return store.dispatch(actions.login(
        fakeUser.email,
        ""))
        .then(() => {
            expect(store.getActions()).toInclude({
                type: 'SET_SESSION_ERROR',
                error: 'Invalid email or password'
            })
        })
});

Thanks for even reading.

Edit:

The setErroraction:

const setError = (error) => ({
  type: 'SET_SESSION_ERROR',
  error,
});

The httpPostmethod:

export const httpPost = (url, data) => (
  fetch(url, {
    method: 'POST',
    headers: createHeaders(),
    body: JSON.stringify(data),
  })
    .then(checkStatus)
    .then(response => response.json())
);

const checkStatus = (response) => {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }

  const error = new Error(response.statusText);
  error.response = response;
  throw error;
};

Solution

  • Because of you are using nested async function in catch method - you need to return the promise:

    .catch(error => {
      return error.response.json()
      .then(data => {
        dispatch(setError(data.error))
      })
    });
    

    Otherwise, dispatch will be called after your assertion.

    See primitive examples:
    https://jsfiddle.net/d5fynntw/ - Without returning
    https://jsfiddle.net/9b1z73xs/ - With returning