Search code examples
reactjstypescriptredux-toolkit

How to mock appSelector with dummy data in React redux-toolkit tests


I have the following react function component:


const AssetsPage = () => {

    const [filteredAssets, setFilteredAssets] = useState<Asset[]>([]);
    const assetsState = useAppSelector(selectAssets);
    ...
    
    useEffect(() => {
        setFilteredAssets(
            assetsState.assets
        );
    }, [assetsState.assets])

    useEffect(() => {
        store.dispatch(fetchAssetsAsync(null));
    }, [])

    
    const columns: GridColDef[] = [
        ...some column definitions...
    ];

  

    return (
        <>
            <Typography variant="h2" ml={2}>
                {t("Asset.Title")}
            </Typography>
            <DataTable
              rows={filteredAssets}
              columns={columns}
              rowId="globalId"
            />
        </>
    )
}

export default AssetsPage

And the corresponding slice:

import {Asset} from "../../models/Asset";
import {createAsyncThunkWithErrorHandling} from "../../utils/thunkHelper";
import {createSlice} from "@reduxjs/toolkit";
import {RootState} from "../../app/store";
import {createAsset, deleteAsset, getAssets, updateAsset} from "../../api/assets";

const initialState = {
    isLoaded: false,
    assets: [] as Asset[],
}

...

export const fetchAssetsAsync = createAsyncThunkWithErrorHandling(
    "assets/fetch",
    async (payload: string) => {
        return await getAssets(payload);
    }
)
...

export const oeeAssetsSlice = createSlice({
    name: "assets",
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder.addCase(fetchAssetsAsync.fulfilled, (state, action) => {
            state.isLoaded = true;
            state.assets = [
                ...action.payload,
            ];
        });
       ...
    }

})
export const selectAssets = (state: RootState) => state.assets;


export default oeeAssetsSlice.reducer;

This works just fine, it loads data from the backend (or, if I replace the fetchAssetsAsync implementation with static data, it loads it properly.)

I am trying to do some unit testing. Rendering the empty table works fine: but I cannot inject data to the assetsState no matter how I try.

import {fireEvent, render, screen, waitFor} from "@testing-library/react";
import {Provider} from "react-redux";
import {RootState, store} from "../../../app/store";
import AssetsPage from "./AssetsPage";
import {createTheme} from "@mui/material/styles";
import {ThemeOptions} from "../../ThemeOptions/ThemeOptions";
import {ThemeProvider} from "@mui/material";
import oeeAssetsReducer, {selectAssets} from "../../../features/assets/oeeAssetsSlice";
import {useAppSelector} from "../../../app/hooks";
import appReducer from "../../../features/app/appSlice";
import userReducer from "../../../features/user/userSlice";

describe("AssetListPage", () => {
    const theme = createTheme(ThemeOptions);

    // This test works fine.
    test("renders the empty component", async () => {
        render(
            <ThemeProvider theme={theme}>
                <Provider store={store}>
                    <AssetsPage/>
                </Provider>
            </ThemeProvider>
        );
        expect(screen.getByText("No rows")).toBeInTheDocument();
    });


    // todo: this doesnt work.
   const asset = {
        globalId: "asset-1",
        description: "test asset",
        businessUnitCode: "bu-1",
        localId: "123",
        enabledFlag: 1,
    };

    jest.mock("../../../app/hooks", () => ({
        useAppSelector: (selectAssets: any) => ({
            assetsState: {
                isLoaded: true,
                assets: [asset]
            }
        })
    }));


    jest.mock("react-i18next", () => ({
        useTranslation: () => ({
            t: (key: string) => key
        })
    }));

    test("renders the component with one data line", async () => {

       render(<ThemeProvider theme={theme}>
            <Provider store={store}>
                <AssetsPage/>
            </Provider>
        </ThemeProvider>
        );

        render(
            <ThemeProvider theme={theme}>
                <Provider store={store}>
                    <AssetsPage/>
                </Provider>
            </ThemeProvider>
        );

        expect(screen.getByText("Asset.GlobalAssetId")).toBeInTheDocument();
        expect(screen.getByText("Asset.Description")).toBeInTheDocument();
        expect(screen.getByText("Asset.BusinessUnit")).toBeInTheDocument();
        expect(screen.getByText("Asset.LocalId")).toBeInTheDocument();  // it renders the "empty table" instead
        expect(screen.getByText("Asset.Disable")).toBeInTheDocument();
        expect(screen.getByText("Asset.Actions")).toBeInTheDocument();
    })
  
});

The test always renders a table without any data. What am I missing here? I cannot install Enzyme, tried feeding this to chatGPT, but nothing worked so far.


Solution

  • Here is a possible solution:

        test ("render the emtpy component with one data line", async () => {
            const mockUser = getAWhoamiSlice_SUPERADMIN()
            const mockShifts = getATestShiftsReducer_EMTPY()
            const mockBusinessUnits =getBusinessUnitsReducer_EMPTY()
            const mockAssets = getAssetsReducer_TWO_ITEMS()
    
            const mockStore = configureStore({
                middleware: undefined,
                reducer: {
                    whoami: whoamiReducer,
                    shifts: oeeShiftsReducer,
                    businessUnits: oeeBusinessUnitReducer,
                    assets: oeeAssetsReducer,
                },
                preloadedState: {
                    whoami: mockUser,
                    shifts: mockShifts,
                    businessUnits: mockBusinessUnits,
                    assets: mockAssets
                },
            });
    
            render(
                <ThemeProvider theme={theme}>
                    <Provider store={mockStore}>
                        <AssetsPage/>
                    </Provider>
                </ThemeProvider>
            );
    
            expect(screen.queryByText("No rows")).not.toBeInTheDocument();
            expect(screen.getByText("Asset.GlobalAssetId")).toBeInTheDocument();
            expect(screen.getByText("Asset.Description")).toBeInTheDocument();
            expect(screen.queryByText(mockAssets.assets[0].name)).toBeInTheDocument();
        })
    
    

    Some explanation for the unseen functions:

    • The reducers are the same reducers used in the application. They are not used however, we preload the state for the tests with initial data.
    • the preloaded state functions are returning similar data which would arrive otherwise with the REST calls.