Unit testing redux toolkit slices

ujjavala

ujjavala

Posted on March 14, 2024

Unit testing redux toolkit slices

Recently, I worked on an application where I chose redux toolkit slices over the traditional way of redux state management (reducer logic, action creators, and action types spread across separate files) and interestingly enough, there were not many resources that explained how to unit test actions, reducers and service logic. Now don't get me wrong. There are resources which dive into the surface of it but most of them assume that the codebase uses createAsyncThunk which in my case wasn't true or are simply too disoriented. I felt that there is a need to get all of it in one single page without all the fuss and here I am doing just that.

Testing actions

My slice file sampleSlice.js contains six actions as shown below. I tested every action by calling the reducer operation upon the test data and verifying whether the result is as expected or not.

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  isLoading: false,
  isError: false,
  isSampleOpted: false,
}

// Slice

export const sampleSlice = createSlice({
  name: 'sampleReducer',
  initialState: initialState,
  reducers: {
    // Give case reducers meaningful past-tense "event"-style names

    sampleFetched(state, action) {
      const isSampleOpted = action.payload?.sample_flag
      state.isSampleOpted = isSampleOpted
    },

    sampleToggled(state, action) {
      state.isSampleOpted = action.payload
    },

    loadingStarted: state => {
      state.isLoading = true
    },

    loadingFinished: state => {
      state.isLoading = false
    },

    errorOccured: state => {
      state.isError = true
    },
    successOccured: state => {
      state.isError = false
    }
  }
})

export default sampleSlice.reducer

export const selectState = state => state.sampleReducer

export const { sampleFetched, sampleToggled, loadingStarted, loadingFinished, errorOccured, successOccured } = sampleSlice.actions
Enter fullscreen mode Exit fullscreen mode

And given below are the unit tests for it

import '@testing-library/jest-dom'
import sampleSlice, { errorOccured, loadingFinished, loadingStarted, sampleFetched, sampleToggled, successOccured } from './sampleSlice'

let state = {
  isSampleOpted: false,
  isLoading: true,
  isError: true,
}

describe('sampleSlice', () => {
  it('initialize slice with initialValue', () => {
    const sampleSliceInit = sampleSlice(state, { type: 'unknown' })
    expect(sampleSliceInit).toBe(state)
  })

  it('test sampleFetched', () => {
    let testData = {
      sample_flag: true,
    }

    const afterReducerOperation = sampleSlice(state, sampleFetched(testData))

    expect(afterReducerOperation.isSampleOpted).toBeTruthy()
  })

  it('test sampleToggled', () => {
    let testData = {
      sample_flag: true
    }
    const afterReducerOperation = sampleSlice(state, sampleToggled(testData))
    expect(afterReducerOperation.isSampleOpted).toBeTruthy()
  })

  it('test loadingStarted', () => {
    const afterReducerOperation = sampleSlice(state, loadingStarted())
    expect(afterReducerOperation.isLoading).toBeTruthy()
  })

  it('test loadingFinished', () => {
    const afterReducerOperation = sampleSlice(state, loadingFinished())
    expect(afterReducerOperation.isLoading).toBeFalsy()
  })

  it('test errorOccured', () => {
    const afterReducerOperation = sampleSlice(state, errorOccured())
    expect(afterReducerOperation.isError).toBeTruthy()
  })

  it('test successOccured', () => {
    const afterReducerOperation = sampleSlice(state, successOccured())
    expect(afterReducerOperation.isError).toBeFalsy()
  })

Enter fullscreen mode Exit fullscreen mode

Testing api calls

My sampleDetails.js file primarily has two api calls fetchSample and toggleSample as shown below:

import axios from 'axios'

import { errorOccured, loadingFinished, loadingStarted, sampleFetched, sampleToggled, successOccured } from '../store/sampleSlice';

export const fetchSample = () => async dispatch => {
    dispatch(loadingStarted());
    try {
        const response = await axios.get('/sample');
        dispatch(successOccured())
        dispatch(sampleFetched(response.data));
    } catch (e) {
        dispatch(errorOccured())
    }
    finally {
        dispatch(loadingFinished())
    }
};

export const toggleSample = (toggleSampleVal) => async dispatch => {
    dispatch(loadingStarted());
    try {
        await axios.post('/sample, {
            sample_flag: toggleSampleVal
        })
        dispatch(successOccured())
        dispatch(sampleToggled(toggleSampleVal));
    } catch (e) {
        dispatch(errorOccured())
    }
    finally {
        dispatch(loadingFinished())
    }
};
Enter fullscreen mode Exit fullscreen mode

Unit testing this was interesting because there are limited resources that targets the test without the use of mock thunks. I tested it by mocking axios and returning mock status codes, thereby asserting it against the dispatched action, however, there might be a better way to do this.

import '@testing-library/jest-dom';
import { fetchSample, toggleSample } from './sampleDetails';
import axios from 'axios';

const testSampleData = {
    'sample_flag': true,
}

jest.mock('axios');

describe('sampleDetails', () => {

    it('should dispatch loadingStarted when starting to fetch sample details', async () => {
        const mockDispatch = jest.fn();
        await fetchSample()(mockDispatch)
        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'sampleReducer/loadingStarted'
        })
    });

    it('should dispatch sampleFetched when sample details are fetched', async () => {
        axios.get.mockImplementationOnce(() =>
            Promise.resolve({
                data: testSampleData
            })
        );
        const mockDispatch = jest.fn();
        await fetchSample()(mockDispatch)
        expect(mockDispatch).toHaveBeenCalledWith({
            payload: testSampleData,
            type: 'sampleReducer/sampleFetched'
        })
    });

    it('should dispatch errorOccured when fetch sample details fails', async () => {
        axios.get.mockImplementationOnce(() =>
            Promise.reject({ statusCode: 500 })
        );
        const mockDispatch = jest.fn();
        await fetchSample()(mockDispatch)
        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'sampleReducer/errorOccured'
        })
    });

    it('should dispatch loadingStarted when starting to toggle sample option', async () => {
        const mockDispatch = jest.fn();
        await toggleSample()(mockDispatch)
        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'sampleReducer/loadingStarted'
        })
    });

    it('should dispatch sampleToggled when sample is toggled successfully', async () => {
        axios.post.mockImplementationOnce(() =>
            Promise.resolve({ statusCode: 200 })
        );
        const mockDispatch = jest.fn();
        await toggleSample()(mockDispatch)
        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'sampleReducer/sampleToggled'
        })
    });

    it('should dispatch errorOccured when sample toggle fails', async () => {
        axios.post.mockImplementationOnce(() =>
            Promise.reject({ statusCode: 500 })
        );
        const mockDispatch = jest.fn();
        await toggleSample()(mockDispatch)
        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'sampleReducer/errorOccured'
        })
    });

    it('should dispatch loadingStarted when notification is starting to be shown', async () => {
        const mockDispatch = jest.fn();
        await showNotification()(mockDispatch)
        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'sampleReducer/loadingStarted'
        })
    });

});
Enter fullscreen mode Exit fullscreen mode

You must have observed that I haven't explicitly used createAsyncThunk anywhere as my application wasn't complex enough to use it. For the store, I did rely on configureStore from redux toolkit, which implicitly takes care of everything as shown below

import { combineReducers } from 'redux'
import { configureStore } from '@reduxjs/toolkit'


import sampleReducer from './sampleSlice'
const reducer = combineReducers({
    sampleReducer,
})

// Automatically adds the thunk middleware and the Redux DevTools extension
const store = configureStore({
    reducer
})
export default store
Enter fullscreen mode Exit fullscreen mode

Hope this article was helpful. If anyone is interested in migrating their old code to redux, I found this really helpful.

Keep calm and unit test!

💖 💪 🙅 🚩
ujjavala
ujjavala

Posted on March 14, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Unit testing redux toolkit slices
unittest Unit testing redux toolkit slices

March 14, 2024