Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Bionic Julia
Bionic Julia

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.

  • ThefetchGamesSummary 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,},//...}
Enter fullscreen modeExit fullscreen mode
  • TheupdateGameInterest 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,}},},})
Enter fullscreen modeExit fullscreen mode

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})
Enter fullscreen modeExit fullscreen mode

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({})})})
Enter fullscreen modeExit fullscreen mode

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)}
Enter fullscreen modeExit fullscreen mode

When writing the test, I needed to:

  • Dispatch thefetchGamesSummary async action.
  • Check the result type wasfulfilled i.e. matches how I defined myextraReducers.
  • Check that the result from the dispatch matches the mock response.
  • Check that thegames 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})})})
Enter fullscreen modeExit fullscreen mode

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 thefetchGamesSummary async action to fill out ourgames state.
  • Dispatch theupdateGameInterest action.
  • Check that thegames 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)})})
Enter fullscreen modeExit fullscreen mode

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,}
Enter fullscreen modeExit fullscreen mode

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Formerly a banker and tech startup founder. Now on my 3rd career as a full stack software engineer (with a front end focus). Posts on tech and things learnt on my programming journey. 👩🏻‍💻
  • Location
    London
  • Work
    Software Engineer
  • Joined

More fromBionic Julia

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp