Search code examples
reactjstypescriptunit-testingreact-testing-libraryredux-toolkit

Trying to render App & getting -> Error: Actions must be plain objects. Use custom middleware for async actions


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.


Solution

  • 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();
    });