Search code examples
reactjsreduxjestjsreact-testing-libraryredux-toolkit

Jest testing react-redux component using React testing library


Hi I am learning React and Redux. I am trying to render a React component to test some user interactions using react test library. The component uses useSelector to get a list of missions from the redux store. I tried to write a reusable test render function as suggested in redux documentation.

This is the element I am trying to render for testing.

const MissionsTable = () => {
  const list = useSelector((state) => state.missions.missions);
  return (
    <div>
      <Table
        striped
        bordered
        hover
        responsive="sm"
        className={styles.missionTable}
      >
        <thead>
          <tr>
            <th>Mission</th>
            <th>Description</th>
            <th>Status</th>
            <th> </th>
          </tr>
        </thead>
        <tbody>
          {list.map((obj) => (
            <Mission
              key={obj.id}
              id={obj.id}
              name={obj.name}
              description={obj.description}
              isReserved={obj.isReserved}
            />
          ))}
        </tbody>
      </Table>
    </div>
  );
};

export default MissionsTable;

This is my missionSlice.

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

const initialState = { missions: [], toFetch: 'true' };

export const fetchMissions = createAsyncThunk('missions/fetchMissions', async () => {
  const response = await fetch('https://api.spacexdata.com/v3/missions');
  return response.json();
});

const missionSlice = createSlice({
  name: 'missions',
  initialState,
  reducers: {
    bookMission: (state, { payload }) => {
      const newArr = state.missions.map((misObj) => {
        if (misObj.id === payload) {
          if (misObj.isReserved === true) {
            return { ...misObj, isReserved: false };
          }
          return { ...misObj, isReserved: true };
        }
        return ({ ...misObj });
      });
      const newState = { ...state };
      return { ...newState, missions: newArr };
    },
  },
  extraReducers(builder) {
    builder.addCase(fetchMissions.fulfilled, (state, action) => {
      const newState = { ...state };
      const missionList = [];
      action.payload.forEach((misObj) => {
        missionList.push({
          id: misObj.mission_id,
          name: misObj.mission_name,
          description: misObj.description,
          wiki: misObj.wikipedia,
          isReserved: false,
        });
      });
      newState.missions = missionList;
      newState.toFetch = false;
      return newState;
    });
  },
});

export const { bookMission } = missionSlice.actions;
export default missionSlice.reducer;

This is how I wrote the helper function to mock store.

function renderWithProviders(
  ui,
  {
    preloadedState = {
      missions: [{
        id: 'testmission1',
        name: 'test1',
        descriptions: 'test mission',
        isReserved: false,
        wiki: '',
      }]
    },
    store = configureStore({ reducer: { missions: missionSliceReducer }, preloadedState }),
    ...renderOptions
  } = {}
) {
  console.log(preloadedState);
  function Wrapper({ children }) {
    return <Provider store={store}>{children}</Provider>
  }

  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}

export default renderWithProviders;

This is the test case I have written to check a button with text.

test('testing Mission page UI', () => {
  renderWithProvider( < MissionsTable / > );
  const missionButtons = screen.findAllByText('Join Mission');
  expect(missionButtons).toHaveLength(1);
});

I am getting this error. I understand the list is undefined. How can I get the data I added in intialstate in the mockstore to be rendered.

TypeError: Cannot read properties of undefined (reading 'map')

      24 |         </thead>
      25 |         <tbody>
    > 26 |           {list.map((obj) => (
         |                 ^
      27 |             <Mission
      28 |               key={obj.id}
      29 |               id={obj.id}

This is how the UI will look like. I have to check user interaction on join mission buttons. enter image description here


Solution

  • The state data structure is { missions: any[]; toFetch: string }, so you should get the missions state slice like:

    const list = useSelector(state => state.missions);
    

    E.g.

    MissionsTable.jsx:

    import React from 'react';
    import { useSelector } from 'react-redux';
    
    const MissionsTable = () => {
      const list = useSelector((state) => state.missions);
      console.log('list: ', list);
      return null;
    };
    
    export default MissionsTable;
    

    missionSlice.js:

    import { createSlice } from '@reduxjs/toolkit';
    
    const initialState = { missions: [], toFetch: 'true' };
    
    const missionSlice = createSlice({
      name: 'missions',
      initialState,
      reducers: {},
    });
    
    export default missionSlice.reducer;
    

    MissionsTable.test.jsx:

    import { configureStore } from "@reduxjs/toolkit";
    import { render } from "@testing-library/react";
    import React from "react";
    import { Provider } from "react-redux";
    import MissionsTable from "./MissionsTable";
    import missionSliceReducer from './missionSlice';
    
    function renderWithProvider(
      ui,
      {
        preloadedState = {
          missions: [{
            id: 'testmission1',
            name: 'test1',
            descriptions: 'test mission',
            isReserved: false,
            wiki: '',
          }]
        },
        store = configureStore({ reducer: { missions: missionSliceReducer }, preloadedState }),
        ...renderOptions
      } = {}
    ) {
      function Wrapper({ children }) {
        return <Provider store={store}>{children}</Provider>;
      }
      return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
    }
    
    describe('75674010', () => {
      test('should pass', () => {
        renderWithProvider(<MissionsTable />);
      })
    })
    

    Test result:

     PASS   redux-toolkit-example  packages/redux-toolkit-example/stackoverflow/75674010/MissionsTable.test.jsx
      75674010
        ✓ should pass (27 ms)
    
      console.log
        list:  [
          {
            id: 'testmission1',
            name: 'test1',
            descriptions: 'test mission',
            isReserved: false,
            wiki: ''
          }
        ]
    
          at MissionsTable (packages/redux-toolkit-example/stackoverflow/75674010/MissionsTable.jsx:6:11)
    
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        2.715 s