reactjsreact-reduxjestjsredux-mock-storemapdispatchtoprops

How can I test that a redux dispatch prop is called for a component on mount using Jest?


I'm trying to write a jest test to test that a dispatch call (fetchData) to an API is called when a component is rendered. The component is setup like this:

import * as React from 'react';
import { connect } from 'react-redux';
import { bindActions } from 'reports/utils/redux';

const connectedComponent = ({ foo, fetchData }) => {
    const loadData = React.useCallback(async () => {
        if (foo) {
            await fetchData(foo.id);
        }
    }, []);
    
    React.useEffect(() => { loadData(); }, []);
    return <></>;
}

const mapStateToProps = (state) => {
    const foo = <retrieves foo from the redux store>
    return { foo };
};

const mapDispatchToProps = bindActions({
    fetchData: (fooId) => api.get({ foo_id: fooId }),
});

export default connect(mapStateToProps, mapDispatchToProps)(connectedComponent);

In my jest/typescript test, I've tried mocking react-redux's connect function (following this https://stackoverflow.com/a/49095870/25441091), but I couldn't figure out a way to pass in the mapDispatchToProps into connectedComponent. I've also tried passing in the dispatch prop (fetchData) to the mockStore as state, but that didn't work either:

...

const middlewares = [ReduxThunk];
const mockStore = configureStore(middlewares);

describe('Connected Component', () => {
    it('should load team usage and limits on mount', async () => {
        let fetchDataMock = jest.fn();
        const store = mockStore({ user: { team_id: 1 }, fetchData: fetchDataMock });
        await act(async () => {
            render(
                <Provider store={store}>
                    <connectedComponent />
                </Provider>,
            );
        });

        console.log(store);
        const actions = store.getActions();
        console.log(actions); // [] This is empty

        expect(fetchDataMock).toHaveBeenCalledTimes(1);
        expect(fetchDataMock).toHaveBeenCalledWith({ team_id: 1 });
    });
});

The error I get in this case is: Actions must be plain objects. Use custom middleware for async actions.


Solution

  • Issues

    At least a couple issues:

    1. The Redux pattern(s) you are using are quite outdated. We don't really use the connect Higher Order Component now, instead we prefer to use the useDispatch and useSelector hooks to dispatch actions and subscribe to state changes. Current Redux is written using Redux-Toolkit.
    2. Using "mock" Redux stores is also now considered outdated. You should test your code using a real Redux store.

    Suggestions

    Update the fetchData action to be a Redux-Toolkit Thunk action.

    import { createAsyncThunk } from "@reduxjs/toolkit";
    
    export const fetchData = createAsyncThunk(
      "fetchData",
      async (fooId, thunkApi) => {
        try {
          return api.get({ foo_id: fooId });
        } catch (error) {
          return thunkApi.rejectWithValue(error);
        }
      }
    );
    

    Update ConnectedComponent component to use the useDispatch hook to dispatch the fetchData action.

    import React from "react";
    import { useDispatch, useSelector } from "react-redux";
    import { fetchData } from "./actions";
    
    const ConnectedComponent = ({ foo }) => {
      const dispatch = useDispatch();
    
      React.useEffect(() => {
        const loadData = () => {
          try {
            dispatch(fetchData(foo.id));
          } catch (error) {
            console.warn(error);
          }
        };
    
        loadData();
      }, []);
    
      return <></>;
    };
    
    export default ConnectedComponent;
    

    Update your unit test(s) to use a real Redux store. You can mock the fetchData action and assert that it was called with the specific payload value.

    import { render } from "@testing-library/react";
    import { Provider } from "react-redux";
    import { store } from "./store";
    
    import ConnectedComponent from "./ConnectedComponent";
    import * as Actions from "./actions";
    
    const fetchDataSpy = jest
      .spyOn(Actions, "fetchData")
      .mockImplementation(() => () => Promise.resolve());
    
    const Providers = ({ children }) => (
      <Provider store={store}>{children}</Provider>
    );
    
    describe("Connected Component", () => {
      it("should load team usage and limits on mount", () => {
        render(<ConnectedComponent foo={{ id: "1234" }} />, {
          wrapper: Providers
        });
    
        expect(fetchDataSpy).toBeCalledWith("1234");
      });
    });
    

    enter image description here