DEV Community

loading...
Cover image for Testing API calls in React

Testing API calls in React

wati_fe profile image Boluwatife Fakorede ・4 min read

You can get part one of this article here. It focuses on Mocking APIs for frontend developers.

In the words of Kent C. Dodds.

The more your tests resemble the way your software is used, the more confidence they can give you. — Kent C. Dodds.

While writing tests, it is best to focus on the use cases of our applications. This way, our tests mimic our users, and we are not focused on the implementation details.

Since we are testing for our application use cases, it is important to test for the interaction with data (Hence API requests).

Previously, to test API requests, we would probably have to mock “window.fetch” or “Axios,”, but our users won’t do that, would they? Hence there should be a better approach.

Mocking API requests with msw

Considering the limitations of mocking out fetch or Axios, it is bliss with a tool like msw, allowing the same mock definition for testing, development, and debugging.

msw intercepts the request on the network level; hence our application or test knows nothing about the mocking.

In the previous article, I demonstrated how to use msw to mock APIs. The good news is, we can use the same mocks for our tests!

Refactoring mock APIs

Let’s get started by refactoring our setup workers since we want to share our mock APIs (API handlers).


import {rest} from 'msw'
import * as todosDB from '../data/todo'

const apiUrl = 'https://todos'

interface TodoBody {
  body: todosDB.ITodo
}

interface TodoId {
  todoId: string
}

interface TodoUpdate extends TodoId {
  update: {
    todo?: string
    completed?: boolean
  }
}

const handlers = [
  rest.get<TodoId>(`${apiUrl}/todo`, async (req, res, ctx) => {
    const {todoId} = req.body
    const todo = await todosDB.read(todoId)
    if (!todo) {
      return res(
        ctx.status(404),
        ctx.json({status: 404, message: 'Todo not found'}),
      )
    }

    return res(ctx.json({todo}))
  }),

  rest.get(`${apiUrl}/todo/all`, async (req, res, ctx) => {
    const todos = await todosDB.readAll()
    return res(ctx.json(todos))
  }),

  rest.post<TodoBody>(`${apiUrl}/todo`, async (req, res, ctx) => {
    const {body} = req.body
    const newTodo = await todosDB.create(body)
    return res(ctx.json({...newTodo}))
  }),

  rest.put<TodoUpdate>(`${apiUrl}/todo/update`, async (req, res, ctx) => {
    const {todoId, update} = req.body
    const newTodo = await todosDB.update(todoId, update)
    return res(ctx.json({todo: newTodo}))
  }),

  rest.delete<TodoId>(`${apiUrl}/todo/delete`, async (req, res, ctx) => {
    const {todoId} = req.body
    const todos = await todosDB.deleteTodo(todoId)
    return res(ctx.json({todos: todos}))
  }),
]

export {handlers}
Enter fullscreen mode Exit fullscreen mode

Now the handlers are alone in a new file, and we can share them between our dev-server and test-server. Let’s update the dev-server.

import {setupWorker} from 'msw'
import {handlers} from './handlers'

export const worker = setupWorker(...handlers)
Enter fullscreen mode Exit fullscreen mode

Our dev-server is a lot shorter now, and everything still works, but we are not ready yet to writing tests; we need to set up a test server. Let’s do that.

Setup test server

import {setupServer} from 'msw/node'
import {handlers} from './handlers'

export const server = setupServer(...handlers)
Enter fullscreen mode Exit fullscreen mode

If you notice, the test-server is different from the dev-server as the “setupServer” is gotten from “msw/node.”

It is good to note that you have to install “whatwg-fetch” as Node.js does not support fetch if you are using the fetch API. For our use-case, we bootstrap our application with create-react-app, which handles this automatically.

We would establish API mocking on the global level by modifying the setupTests.ts file (provided by create-react-app) as shown below.

import '@testing-library/jest-dom';
import { server } from './server/test-server';


// Establish API mocking before all tests.
beforeAll(() => server.listen())

// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())

// Clean up after the tests are finished.
afterAll(() => server.close())
Enter fullscreen mode Exit fullscreen mode

NB: You can establish a global level for API mocking if you are not using create-react-app by following the docs.

Testing React API calls.

Let’s test our todos rendering and adding a new todo.


import {TodoPage} from '../todo.screen'
import * as todosDB from '../../data/todo'
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {act} from 'react-dom/test-utils'

test('should renders all todos', async function () {
  const testTodos = await todosDB.readAll()
  render(<TodoPage />)
  const todosContent = await waitFor(() =>
    screen.getAllByTestId('todo-details').map(p => p.textContent),
  )
  const testTodoContent = testTodos.map(c => c.todo)
  expect(todosContent).toEqual(testTodoContent)
})

test('should add a new todo', async function () {
  render(<TodoPage />)
  const input = screen.getByLabelText(/add a todo/i)
  const form = screen.getByRole('form')
  userEvent.type(input, 'add todo')
  act(() => {
   fireEvent.submit(form)
  })

  const allTodos = await waitFor(() => screen.getAllByTestId('todo-details'))
  const newTodo = allTodos.find(p => p.textContent === 'add todo')

  expect(newTodo).toHaveTextContent('add todo')
  expect(allTodos.length).toBe(3)
})
Enter fullscreen mode Exit fullscreen mode

In the above test, we don’t have to mock out “fetch” or “Axios.” We are testing exactly how our users will use the application, a real API request is made, and we get the mock response which is great and gives us much more confidence.

Thank you for reading.

Discussion (2)

pic
Editor guide
Collapse
andrewbaisden profile image
Andrew Baisden

You did a good job explaining this.

Collapse
wati_fe profile image
Boluwatife Fakorede Author

Thank you Andrew.