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.
At least a couple issues:
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.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");
});
});