ujjavala
Posted on March 14, 2024
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
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 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 really helpful.
Keep calm and unit test!
Posted on March 14, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.