โ† Go back
April 04, 2022 ยท 8 mins read min read

Redux Toolkit Testing Async actions

Redux Toolkit how to test async actions using React Testing Library and MSW

redux-toolkit-testing-async-actions

Hello everyone ๐Ÿ‘‹, this post is a continuation of our testing saga with redux-toolkit, here we will continue our previous post now focusing on testing our async actions.

in case you haven't read the previous post I recommend you give it a read because we'll be here using the same application and increasing our test coverage โœ….

Creating the API with JSON Server

Now let's make a real call to our endpoint, for that, I'm going to use a tool called JSON Server that allows us to create an API in less than 30 seconds.

Let's create an endpoint called /results which will return our list of items, to do that we will create a file called 'db.json' in the root level of our project, and .

{
  "results": [
    {
      "id": 0,
      "price": 88782,
      "review": 96323,
      "rating": 43708,
      "distance": 38824,
      "place": "Daytona Beach",
      "name": "Rafael Oberbrunner",
      "image": "https://a0.example.com/image0.jpg"
    }
    // .....
  ]
}

Then in our package.json we will create a new script to start the API server.


"scripts": {
    "start": "react-scripts start",
    ...
    "start:server": "json-server --watch db.json --port 3001" // ๐Ÿ‘ˆ here we are using json-server in the port 3001
  },

Testing Async Actions

if you've created an application with redux in the past, you are probably already familiar with the concept of thunks and using tools like redux-mock-store to create a mock from the store

although functional this approach brings a series of other problems, you have to configure a store along with middleware, the problem is here is when we need to test 'async' actions, and unless your application does not receive any external information, at some point you will have to interact with some API, test this case just with mock is not ideal,

if you still use redux-mock-store an approach that does sense and adopts dependency injection, for libs that make API call

That being said, seeing how the ReactJS ecosystem has changed a lot in recent years, today we have tools like react-testing-library that encourages us to think under integrated tests let's look here at how they work in practice.

first, let's create our render helper to make our tests easier

// testUtils.jsx
import React from 'react'
import { render as rtlRender } from '@testing-library/react'
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'

import homeReducer from './features/home/HomeSlice'

const render = (
  ui: React.ReactElement,
  {
    initialState,
    store = configureStore({
      reducer: { home: homeReducer },
      preloadedState: initialState,
    }),
    ...renderOptions
  } = {}
) => {
  function Wrapper({ children }: { children: React.ReactNode }) {
    return <Provider store={store}>{children}</Provider>
  }
  return {
    ...rtlRender(ui, { wrapper: Wrapper, ...renderOptions }),
    // adding `store` to the returned utilities to allow us
    // to reference it in our tests (just try to avoid using
    // this to test implementation details).
    store,
  }
}

export * from '@testing-library/react'

export { render }

note here we are using our real store,

the second step is to configure `msw` to intercept the `API` calls in our case here we are going to intercept the call to /results let's go ahead and create a new test file for our integration tests, which we can call Home.spec.tsx

// Home.spec.tsx
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { resultsMock } from '../../mocks/results'
import { render, screen, fireEvent } from '../../test-utils'

import Home from './Home'

const { click, change } = fireEvent
const { queryByText, getByText, getByTestId, getAllByTestId } = screen

const BASE_URL = 'http://localhost:3001'

export const handlers = [
  rest.get(`${BASE_URL}/results`, (_, res, ctx) => {
    return res(ctx.json(resultsMock), ctx.delay(150))
  }),
]

const server = setupServer(...handlers)

see that we have created a handle function that will intercept the call to /results and return our mock data, and we have also added a delay to simulate a slow network connection, we also destructure the ` click` and ` change` events from `fireEvent` and `screen` to make our tests more readable.

next, we will create a test that will make sure that the API call is being made correctly, and we will also check that the results are being displayed correctly for that we will use our custom render to run the integration tests.

// Home.spec.tsx

import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { resultsMock } from '../../mocks/results' // mock file with the results simulated
import { render, screen, fireEvent } from '../../test-utils'

import Home from './Home'

const { click, change } = fireEvent
const { queryByText, getByText, getByTestId, getAllByTestId } = screen

const BASE_URL = 'http://localhost:3001' // base url for the API

export const handlers = [
  rest.get(`${BASE_URL}/results`, (_, res, ctx) => {
    return res(ctx.json(resultsMock), ctx.delay(150)) // delay to simulate a slow network connection
  }),
]

const server = setupServer(...handlers)

afterAll(() => server.close()) // close the server after all the tests
beforeAll(() => server.listen()) // start the server before all the tests
afterEach(() => server.resetHandlers()) // reset the handlers after each test

describe('Home component', () => {
  it('should show loading state', async () => {
    render(<Home />)
    expect(getByText(/loading../i)).toBeInTheDocument()
  })

  it('should show loading state', async () => {
    render(<Home />)
    await sleep(200)
    expect(queryByText(/loading.../i)).not.toBeInTheDocument()
    expect(getByText(/Yolanda Little/i)).toBeInTheDocument()
  })

  it('should filter results', async () => {
    render(<Home />)
    await sleep(200)
    change(getByTestId('search'), { target: { value: 'Beach' } })
    expect(getAllByTestId('location-name')).toHaveLength(2)
  })

  it('should show empty list when no results', async () => {
    render(<Home />)
    await sleep(200)
    change(getByTestId('search'), { target: { value: 'xxxx' } })
    expect(getByText(/No results found/i)).toBeInTheDocument()
  })

  it('should filter list by favorites', async () => {
    render(<Home />)
    await sleep(200)
    click(getByText(/Show Favorites/i))
    expect(getByText(/No results found/i)).toBeInTheDocument()

    click(getByText(/Show All/i))
    expect(getAllByTestId('location-name')).toHaveLength(3)
  })
})

Amazing ๐ŸŽ‰ now our tests are passing, with that, we can check the results of our tests and see how the application behaves when we are interacting with it.

Different Testing Philosophies

as we can see here the main difference in testing philosophy and that we have now inverted the concept of 'isolation' by not testing redux directly treating it as just an implementation detail what I find amazing with this approach is that we started to test real behavior of the application, thinking about how a user can interact with it, for users it doesn't matter which framework you use to connect with the API, or which action you create to make the request what matters most to the user is "when interacting in an X way I see a Y response"

creating tests like this brings us great flexibility, we can change these tools mentioned above without our tests needing to be changed forcing us to always test what matters in our code, and most importantly bringing us a higher level of confidence about our code.

Recap

Boom app working, tests passing, really makes me feel so much more confident, and with that let's just recap some things we saw here in this post

  1. for pure functions (reducers, selectors) we can use unit tests in isolation
  2. for async actions, we can use tests integration

with the example we have now, it is easy to see that we can apply these two approaches in our applications, using isolated tests to test our actions and reducers, and use integrated tests to test our asynchronous flames, I find it amazing how testing culture has evolved on the frontend not long ago it was very rare to see tests in applications with javascript mostly because of the difficult setup, and now with tools like react-testing-library tests are not only common but also expected from applications like this.

if you made it this far, my heartfelt thanks, help me by sharing this post and tell me your opinion here, my goal is to share and learn so don't let it share and follow me on social networks that will be here in short.