Unit testing redux toolkit slices
Tags: unittest, redux, webdev, jest
Published: 2024-03-14 10:24:19

Recently, I worked on an application where I chose redux toolkit [slices](https://redux-toolkit.js.org/api/createSlice) 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](https://redux-toolkit.js.org/api/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
```

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

```

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

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'
        })
    });

});
```
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](https://redux-toolkit.js.org/api/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
```

Hope this article was helpful. If anyone is interested in migrating their old code to redux, I found [this](https://redux.js.org/usage/migrating-to-modern-redux#reducers-and-actions-with-createslice) really helpful. 

Keep calm and unit test!