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}
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)
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)
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())
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)
})
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.
Top comments (3)
You did a good job explaining this.
Thank you Andrew.
Thanks Andrew