I am creating vite + typescript + redux app and I have a problem with testing my App component to render, using vitest for testing. I use redux@toolkit and get a problem, when trying to use async thunk in the app component:
Error: Actions must be plain objects. Use custom middleware for async actions.
❯ dispatch node_modules/redux-mock-store/lib/index.js:41:19
❯ getAirports src/App.tsx:19:17
17| if (airportsStatus === "idle") {
18| const getAirports = async () => {
19| await dispatch(fetchAirports());
| ^
20| };
21|
Here is how my test file is configured:
import App from "../App";
import { it } from "vitest";
import { Provider } from "react-redux";
import { render } from "@testing-library/react";
import configureStore from "redux-mock-store";
import {
ThunkDispatch,
UnknownAction,
} from "@reduxjs/toolkit";
import { RootState } from "../common/types";
import { store } from "../app/store";
import thunk from "redux-thunk"
const mockStore = configureStore<
RootState,
ThunkDispatch<RootState, undefined, UnknownAction>
>();
it("should have hello world", async () => {
const initialState: RootState = {
airports: {
airports: [{
id: 100,
code: "TBS",
title: "Tbilisi International Airport"
}],
status: "idle",
error: null,
},
bookings: {
bookings: [{
firstName: "Alexis",
lastName: "Doe",
departureAirportId: 100,
arrivalAirportId: 101,
departureDate: "2024-12-29T00:00:00.000Z",
returnDate: "2024-12-30T00:00:00.000Z"
}],
status: "idle",
error: null,
},
};
const store = mockStore(initialState);
render(
<Provider store={store}>
<App />
</Provider>
);
// expect(getByText("Hello World!")).not.toBeNull();
});
And here is my App component:
const App = () => {
const dispatch = useAppDispatch();
const effectRan = useRef(false);
const airportsStatus = useAppSelector(getAirportsStatus);
useEffect(() => {
if (effectRan.current === false) {
if (airportsStatus === "idle") {
const getAirports = async () => {
await dispatch(fetchAirports());
};
getAirports();
// sync () => {
// await dispatch(fetchAirports());
// })()
}
return () => {
effectRan.current = true;
};
}
}, [dispatch, airportsStatus]);
return (
<>
<Form />
<ListWithBookings />
</>
);
}
export default App;
Here is how my redux store is configured:
import { configureStore } from "@reduxjs/toolkit";
import airportsReducer from "../features/airports/airportsSlice";
import bookingsReducer from "../features/bookings/bookingsSlice";
export const store = configureStore({
reducer: {
airports: airportsReducer,
bookings: bookingsReducer,
}
});
This problem is bothering me all day and I just can't solve it. Thank you!
I tried many things, but right now it doesn't give me any error about types or something, the simple problem is this fetch.
Your "mock store" isn't configured to handle asynchronous actions, e.g. it's not using the Thunk middleware. See redux-mock-store
Asynchronous Actions.
import configureStore from "redux-mock-store";
import { thunk } from "redux-thunk"; // v3+
// import thunk from "redux-thunk"; // v1/v2
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
import App from "../App";
import { it } from "vitest";
import { Provider } from "react-redux";
import { render } from "@testing-library/react";
import configureStore from "redux-mock-store";
import {
ThunkDispatch,
UnknownAction,
} from "@reduxjs/toolkit";
import { RootState } from "../common/types";
import { store } from "../app/store";
import thunk from "redux-thunk"
const middlewares = [thunk];
const mockStore = configureStore<
RootState,
ThunkDispatch<RootState, undefined, UnknownAction>
>(middlewares);
it("should have hello world", async () => {
const initialState: RootState = {
airports: {
airports: [{
id: 100,
code: "TBS",
title: "Tbilisi International Airport"
}],
status: "idle",
error: null,
},
bookings: {
bookings: [{
firstName: "Alexis",
lastName: "Doe",
departureAirportId: 100,
arrivalAirportId: 101,
departureDate: "2024-12-29T00:00:00.000Z",
returnDate: "2024-12-30T00:00:00.000Z"
}],
status: "idle",
error: null,
},
};
const store = mockStore(initialState);
render(
<Provider store={store}>
<App />
</Provider>
);
// expect(getByText("Hello World!")).not.toBeNull();
});
Be aware that mocking a Redux store is no longer the recommended method. See Writing Tests for details. You should instantiate an actual legitimate store, the same way your app does.
import App from "../App";
import { it } from "vitest";
import { Provider } from "react-redux";
import { render } from "@testing-library/react";
import { configureStore } from "@reduxjs/toolkit";
import airportsReducer from "../features/airports/airportsSlice";
import bookingsReducer from "../features/bookings/bookingsSlice";
import { RootState } from "../common/types";
const reducer = {
airports: airportsReducer,
bookings: bookingsReducer,
};
const preloadedState: RootState = {
airports: {
airports: [{
id: 100,
code: "TBS",
title: "Tbilisi International Airport"
}],
status: "idle",
error: null,
},
bookings: {
bookings: [{
firstName: "Alexis",
lastName: "Doe",
departureAirportId: 100,
arrivalAirportId: 101,
departureDate: "2024-12-29T00:00:00.000Z",
returnDate: "2024-12-30T00:00:00.000Z"
}],
status: "idle",
error: null,
},
};
it("should have hello world", async () => {
const store = configureStore({
preloadedState,
reducer,
});
render(
<Provider store={store}>
<App />
</Provider>
);
// expect(getByText("Hello World!")).not.toBeNull();
});