Posted on • Originally published atbionicjulia.com on
Writing Jest Tests For a Redux Toolkit Slice
I've been doing a fair amount of work recently with Redux Toolkit (RTK), for a new feature I'm building. I'm also trying to be a lot stricter with ensuring I've got tests for all the key parts of the code I've written, and so, have also been delving deeper into writing Jest tests for RTK.
The way I learn how to write tests is by following along to good examples. I therefore thought I'd write this blog post as a way to help others who might also be going through this process, but also as a record for myself, as I'm sure I'll be writing similar tests in the future.
Scene setting
To set the context, let's say we've set up our RTK slice for a gaming app we're creating. ThisGames
slice has a state that's basically an object of objects. It allows for an asynchronousfetchGamesSummary
action that calls an external API, and a synchronousupdateGameInterest
action.
- The
fetchGamesSummary
async thunk is called with auserId
and returns a list of games that looks like this:
{call_of_duty:{interest_count:10,key:"call_of_duty",user_is_interested:true,},god_of_war:{interest_count:15,key:"god_of_war",user_is_interested:false,},//...}
- The
updateGameInterest
action is effected by a button toggle, where a user is able to toggle whether they are interested (or not) in a game. This increments/decrements theinterestCount
, and toggles theuserIsInterested
value between true/false. Note, the camelcase is because it relates to frontend variable. Snake case is what's received from the API endpoint.
import{createAsyncThunk,createSlice,PayloadAction}from'@reduxjs/toolkit'exportconstinitialStateGames:TStateGames={games:{},}exportconstfetchGamesSummary=createAsyncThunk('games/fetch_list',async(userId:string)=>{constresponse=awaitgamesService.list(userId)returnresponse})exportconstgamesSlice=createSlice({initialState:initialStateGames,name:'Games',reducers:{updateGameInterest:(state,action:PayloadAction<TUpdateGameInterestAction>)=>({...state,games:{...state.games,[action.payload.gameKey]:{...state.games[action.payload.gameKey],interest_count:state.games[action.payload.gameKey].interest_count+action.payload.interestCount,user_is_interested:action.payload.userIsInterested,},},}),},extraReducers:{[fetchGamesSummary.fulfilled.type]:(state,action:{payload:TGames})=>{constgames=action.payloadreturn{...state,games,}},},})
I haven't shown it here, but upon defining your new slice, you're also going to need to ensure the reducer is added to yourcombineReducers
. e.g.
exportdefaultcombineReducers({games:gamesSlice.reducer,// your other reducers})
Side note: If you want to see the types, scroll down to the Appendix below.
Jest tests
There are a few different things I want to test my RTK slice for. My tests'describe
looks like this:
- Games redux state tests...
- Should initially set games to an empty object.
- Should be able to fetch the games list for a specific user.
- Should be able to toggle interest for a specific game.
Should initially set games to an empty object
I'm going to assume you've already got your Jest config setup for your app. This first test checks that we can connect to our store and specific slice.
importstorefrom'./store'describe('Games redux state tests',()=>{it('Should initially set games to an empty object',()=>{conststate=store.getState().gamesexpect(state.games).toEqual({})})})
Yourstore
is where you set up yourconfigureStore
. See the documentationhere for more info.getState()
is a method that returns the current state tree, from which I'm particularly interested in thegames
slice.
Should be able to fetch the games list for a specific user
This test requires some initial setup as we'll be calling an external API. This bit might differ for you, as it'll depend on how you call your API. I have mine set up through anApiClient
class, which I use to set up my base API Axios settings. If you're interested in learning more about this, read my previous blog post onAxios wrappers. In this app, I've defined agetClient()
method within myApiClient
class that returns anAxiosInstance
.
For the purposes of testing, I don't actually want to make an API call, so I mocked the API request through the use ofaxios-mock-adapter
. There are other packages available, so browse around for whatever works best for you. TheMockAdaptor
takes in an Axios instance as an argument, and from there, enables you to mock call your GET endpoint with your defined mock response. Note here that the API endpoint/games/list/?user_id=${userId}
is in effect what mygamesService.list(userId)
calls in myfetchGamesSummary
function above.
importApiClientfrom'../api/ApiClient'importMockAdapterfrom'axios-mock-adapter'importstorefrom'../../store'constuserId='test123'constgetListResponse={game_1:{interest_count:0,key:'game_1',user_is_interested:false,},}constapiClient=newApiClient()constmockNetworkResponse=()=>{constmock=newMockAdapter(apiClient.getClient())mock.onGet(`/games/list/?user_id=${userId}`).reply(200,getListResponse)}
When writing the test, I needed to:
- Dispatch the
fetchGamesSummary
async action. - Check the result type was
fulfilled
i.e. matches how I defined myextraReducers
. - Check that the result from the dispatch matches the mock response.
- Check that the
games
state reflects what I fetched from the API.
Putting it all together then...
importApiClientfrom'../api/ApiClient'importMockAdapterfrom'axios-mock-adapter'importstorefrom'../../store'// import your slice and typesconstuserId='test123'constgetListResponse={game_1:{interest_count:0,key:'game_1',user_is_interested:false,},}constapiClient=newApiClient()constmockNetworkResponse=()=>{constmock=newMockAdapter(apiClient.getClient())mock.onGet(`/games/list/?user_id=${userId}`).reply(200,getListResponse)}describe('Games redux state tests',()=>{beforeAll(()=>{mockNetworkResponse()})it('Should be able to fetch the games list for a specific user',async()=>{constresult=awaitstore.dispatch(fetchGamesSummary(userId))constgames=result.payloadexpect(result.type).toBe('games/fetch_list/fulfilled')expect(games.game_1).toEqual(getListResponse.game_1)conststate=store.getState().gamesexpect(state).toEqual({games})})})
Should be able to toggle interest for a specific game
With everything set up nicely now, this final test is relatively simpler to write. Just be sure to include thebeforeAll
block calling themockNetworkResponse()
(since ultimately, all your tests will be in this one file).
When writing this test, I needed to:
- Dispatch the
fetchGamesSummary
async action to fill out ourgames
state. - Dispatch the
updateGameInterest
action. - Check that the
games
state updates theinterestCount
anduserIsInterested
values correctly.
importApiClientfrom'../api/ApiClient'importMockAdapterfrom'axios-mock-adapter'importstorefrom'../../store'// import your slice and typesconstuserId='test123'constgetListResponse={game_1:{interest_count:0,key:'game_1',user_is_interested:false,},}constapiClient=newApiClient()constmockNetworkResponse=()=>{constmock=newMockAdapter(apiClient.getClient())mock.onGet(`/games/list/?user_id=${userId}`).reply(200,getListResponse)}describe('Games redux state tests',()=>{beforeAll(()=>{mockNetworkResponse()})it('Should be able to toggle interest for a specific game',async()=>{awaitstore.dispatch(fetchGamesSummary(userId))store.dispatch(gamesSlice.actions.updateGameInterest({interestCount:1,userIsInterested:true,gameKey:'game_1',}),)letstate=store.getState().gamesexpect(state.games.game_1.interest_count).toBe(1)expect(state.games.game_1.userIsInterest).toBe(true)store.dispatch(gamesSlice.actions.updateGameInterest({interestCount:-1,userIsInterested:false,gameKey:'game_1',}),)state=store.getState().gamesexpect(state.games.game_1.interest_count).toBe(0)expect(state.games.game_1.userIsInterest).toBe(false)})})
And that's it! I came up with this example solely for the purpose of this blog post, so didn't actually test that the code works. 😅 If you come across any suspected errors, let me know. Or, if you come up with a better way of testing my cases, I'd be all ears! 😃
Talk to me onTwitter,Instagram or my websitehttps://bionicjulia.com
Appendix
Types
exporttypeTGame={interest_count:number,key:string,user_is_interested:boolean,}exporttypeTGames={string:TGame}|{}exporttypeTStateGames={games:TGames,}exporttypeTUpdateGameInterestAction={gameKey:string,userIsInterested:boolean,interestCount:number,}
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse