Skip to main content
Unit testing React apps with TypeScript, Vite, Jest, Redux toolkit, and Axios
13 Nov 2024

Efficient and reliable applications can do wonders for your business. Unit testing can be the key to building React applications that are both efficient and reliable. Testing individual components in isolation allows you to catch and fix bugs early, improve code quality, and accelerate your development process. This guide walks you through configuring unit tests for a React application using Jest, with TypeScript support and Vite as the build tool. We’ll also cover mocking Axios for API calls and testing Redux slices with @reduxjs/toolkit

Steps for setting up your development environment for unit testing

Ensure you have a Vite project already set up. Let’s dive into configuring Jest and writing tests for API calls and React components. 

Step 1: Install the required dependencies

  • Jest and TypeScript support: npm install --save-dev jest @types/jest ts-jest 
  • React testing utilities: npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event @types/testing-library__react @types/testing-library__user-event @jest/globals 
  • Mocking Axios: npm install --save-dev axios-mock-adapter axios 

Step 2: Configure Jest with TypeScript Support 

Create a jest.config.js file in the root directory to set up Jest: 

export default { 
    testEnvironment: "jsdom", 
    transform: { 
        "^.+\\.tsx?$": "ts-jest", 
    }, 
    moduleNameMapper: { 
        "\\.(css|less|sass|scss)$": "identity-obj-proxy", 
        "^.+\\.svg$": "jest-transformer-svg", 
        "^@/(.*)$": "<rootDir>/src/$1", 
        "^@components/(.*)$": "<rootDir>/src/components/$1", 
    }, 
    setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], 
};

Create a jest.setup.ts file to handle any Jest environment setup:

import "@testing-library/jest-dom/extend-expect"; 
 
//setting to get value from local storage which are encrypted/decrypted using window.atob/window.btoa 
(global as any).window = { 
  localStorage: { 
    getItem: jest.fn(), 
    setItem: jest.fn(), 
    clear: jest.fn(), 
  }, 
  btoa: (str: string) => Buffer.from(str, 'binary').toString('base64'), 
  atob: (str: string) => Buffer.from(str, 'base64').toString('binary'), 
  location: { 
    href: '', 
  }, 
}; 
 
global.XMLHttpRequest = undefined;

Add a test script in package.json to run the tests:

"scripts": { 
    "test": "jest --coverage" 
  },

Step 3: Configure the Redux Store and Middleware 

Create store.ts:

import { combineReducers, configureStore } from "@reduxjs/toolkit"; 
import counterSlice from "../components/login/LoginSlice"; 
 
const rootReducer = combineReducers({ 
  counter: counterSlice.reducer, 
}) 
 
export function setupStore(preloadedState?: Partial<any>) { 
  return configureStore({ 
    reducer: rootReducer, 
    preloadedState, 
  }); 
} 
 
export type RootStateType = ReturnType<typeof rootReducer>; 
export type AppDispatchType = ReturnType<typeof setupStore> 
export type AppDispatch = AppDispatchType['dispatch']

Create hooks.ts for typed hooks:

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 
import type { AppDispatch, RootStateType } from './store'; 
 
// Use throughout your app instead of plain `useDispatch` and `useSelector` 
export const useAppDispatch = () => useDispatch<AppDispatch>(); 
export const useAppSelector: TypedUseSelectorHook<RootStateType> = useSelector;

Step 4: Configure Axios Middleware

Create ApiHandler.ts:

import Axios, { AxiosResponse, InternalAxiosRequestConfig } from "axios"; 
 
export const axiosAPI = Axios.create({ 
    baseURL: import.meta.env.VITE_BASE_URL, 
}); 
 
const requestHandler = (request: InternalAxiosRequestConfig) => { 
    const token = "TOKEN" 
    request.headers['content-Type'] = 'application/json'; 
    if (token) { 
        request.headers['access-Token'] = `bearer ${token}`; 
    } 
    return request; 
}; 
 
axiosAPI.interceptors.request.use(request => requestHandler(request)); 
axiosAPI.interceptors.response.use( 
    response => { 
        //to send only headers and data 
        return { data: response.data, headers: response.headers } as AxiosResponse; 
    }, 
    err => { 
        const { response } = err; 
        if (response?.status === 401) { 
            // clear the session 
            // navigate to login 
        } 
        return Promise.reject(err?.response?.data || err); 
    }, 
);

Step 5: Create a Redux slice 

Create CounterSlice.ts:

import { PayloadAction, createSlice, createAsyncThunk } from "@reduxjs/toolkit"; 
import { axiosAPI } from "../../shared/ApiHandler"; 
 
export interface IInitialCounterState { value: number; } 
 
export interface ICounterChangeResponse { previousValue: number, updatedValue: number } 
 
export const onChangedBy = createAsyncThunk<ICounterChangeResponse, number, { rejectValue: string }>( 
    "counter/changedBy", 
    async (payload, thunkAPI) => { 
        try { 
            const response = await axiosAPI.post("API_URL", payload) 
            return response.data 
        } catch (err) { 
            const error = err as string; 
            thunkAPI.rejectWithValue(error); 
        } 
    } 
); 
 
const initialState: IInitialCounterState = { value: 0 }; 
 
const counterSlice = createSlice({ 
    name: 'counter', 
    initialState, 
    reducers: {}, 
    extraReducers: (builder) => { 
        builder.addCase(onChangedBy.fulfilled, (state, action: PayloadAction<ICounterChangeResponse>) => { 
            state.value = action.payload.updatedValue 
            //any other action to be performed on fulfilled action  
        }) 
    } 
}); 
 
export default counterSlice

Step 6: Write test utility

Create test-utils.tsx to wrap components with Redux providers: 

import React, { PropsWithChildren } from 'react'; 
import { render } from '@testing-library/react'; 
import type { RenderOptions } from '@testing-library/react'; 
import { Provider } from 'react-redux'; 
import { setupStore } from './store'; 
import type { AppDispatchType, RootStateType } from './store'; 
 
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> { 
  preloadedState?: Partial<RootStateType>; 
  store?: AppDispatchType; 
} 
 
export function renderWithProviders(ui: React.ReactElement,{preloadedState = {},store = setupStore(preloadedState),...renderOptions}: ExtendedRenderOptions = {}) { 
  function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element { 
    return <Provider store={store}>{children}</Provider>; 
  } 
  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; 
}

Step 7: Create and test a counter component

Create a Counter.tsx component:

import React from 'react'; 
import { useAppDispatch, useAppSelector } from './redux/hooks'; 
import { onChangedBy } from './CounterSlice'; 
 
const Counter = () => { 
  const count = useAppSelector(state => state.counter.value); 
  const dispatch = useAppDispatch(); 
 
  return ( 
    <div> 
      <h1 data-testid="countValue">Count: {count}</h1> 
      <button data-testid="incrementButton" onClick={() => dispatch(onChangedBy(5))}>Increment by 5</button> 
      <button data-testid="decrementButton" onClick={() => dispatch(onChangedBy(5))}>Decrement by 5</button> 
    </div> 
  ); 
}; 
 
export default Counter;

Create the Counter.test.tsx file to write unit tests:

import { act, fireEvent, screen } from '@testing-library/react'; 
import axiosMockAdapter from 'axios-mock-adapter'; 
import Counter from './Counter'; 
import { axiosAPI } from './shared/ApiHandler'; 
import { setupStore } from './redux/store'; 
import { renderWithProviders } from './redux/test-utils'; 
 
describe('Counter Component', () => { 
    const mockAxios = new axiosMockAdapter(axiosAPI); 
    let store; 
    store = setupStore(); 
    beforeEach(() => { 
        jest.clearAllMocks(); 
        mockAxios.reset(); 
    }); 
 
    test('renders the component', async () => { 
        renderWithProviders(<Counter />, { store }); 
        expect(screen.getByTestId('countValue')).toHaveTextContent('Count: 0'); 
    }); 
 
    test('Increment', async () => { 
        const mockResponse = { previousValue: 10, updatedValue: 15 } 
        mockAxios.onPost("API_URL", 5).reply(200, mockResponse); 
        renderWithProviders(<Counter />, { store }); 
        await act(() => { 
            fireEvent.click(screen.getByTestId('incrementButton')) 
        }) 
        expect(screen.getByTestId('countValue')).toHaveTextContent('Count: 15'); 
    }); 
 
    test('Decrement', async () => { 
        const mockResponse = { previousValue: 15, updatedValue: 10 } 
        mockAxios.onPost("API_URL", 5).reply(200, mockResponse); 
        renderWithProviders(<Counter />, { store }); 
        await act(() => { 
            fireEvent.click(screen.getByTestId('decrementButton')) 
        }) 
        expect(screen.getByTestId('countValue')).toHaveTextContent('Count: 10'); 
    }); 
});

Step 8: Fix the import.meta issue in Vite

If you encounter the following error: 
error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'. 
         baseURL: import.meta.env.VITE_BASE_URL

You get the same error even after setting the module value. To fix this issue, use the vite-plugin-environment.

Install and configure vite-plugin-environment: 
npm install --save-dev vite-plugin-environment

Update vite.config.ts: 

import { defineConfig } from 'vite'; 
import react from '@vitejs/plugin-react'; 
import tsconfigPaths from "vite-tsconfig-paths"; 
import EnvironmentPlugin from 'vite-plugin-environment'; 
 
export default defineConfig({ 
  plugins: [react(), tsconfigPaths(),EnvironmentPlugin('all')], 
})

Update ApiHandler.ts: use the environment variable below

baseURL: process.env.VITE_BASE_URL

 

With the configuration complete, you can now run your tests using the command: 
npm run test 

You will get the result as shown below:

This setup ensures that your React components, Redux slices, and API interactions are thoroughly tested, giving you confidence in the reliability of your application. 

Subscribe to our feed

select webform