How to mock API requests in React tests
Mocking API requests in React tests ensures components render correctly with different data scenarios without making real network calls. With over 12 years of React development experience since 2014 and as the creator of CoreUI, I’ve written comprehensive test suites with extensive API mocking. Jest and Mock Service Worker (MSW) provide powerful tools to intercept and mock HTTP requests at different levels. This approach creates fast, reliable tests that don’t depend on external APIs or network availability.
Use Jest mocks or MSW to intercept and mock API requests in React component tests.
Mocking fetch with Jest:
// UserProfile.jsx
import { useState, useEffect } from 'react'
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
})
.then(data => {
setUser(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [userId])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return <div>No user found</div>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
Test with Jest fetch mock:
// UserProfile.test.jsx
import { render, screen, waitFor } from '@testing-library/react'
import UserProfile from './UserProfile'
global.fetch = jest.fn()
describe('UserProfile', () => {
beforeEach(() => {
fetch.mockClear()
})
it('displays user data from API', async () => {
const mockUser = { name: 'John Doe', email: '[email protected]' }
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
})
render(<UserProfile userId={1} />)
expect(screen.getByText('Loading...')).toBeInTheDocument()
expect(await screen.findByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('[email protected]')).toBeInTheDocument()
expect(fetch).toHaveBeenCalledWith('/api/users/1')
expect(fetch).toHaveBeenCalledTimes(1)
})
it('handles API errors', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'))
render(<UserProfile userId={1} />)
expect(await screen.findByText('Error: Network error')).toBeInTheDocument()
})
it('handles 404 response', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 404
})
render(<UserProfile userId={999} />)
expect(await screen.findByText('Error: Failed to fetch')).toBeInTheDocument()
})
})
Mocking axios:
// PostList.jsx
import { useState, useEffect } from 'react'
import axios from 'axios'
export default function PostList() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
axios.get('/api/posts')
.then(res => {
setPosts(res.data)
setLoading(false)
})
.catch(err => {
console.error(err)
setLoading(false)
})
}, [])
if (loading) return <div>Loading posts...</div>
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
// PostList.test.jsx
import { render, screen } from '@testing-library/react'
import axios from 'axios'
import PostList from './PostList'
jest.mock('axios')
describe('PostList', () => {
it('displays posts from API', async () => {
const mockPosts = [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' }
]
axios.get.mockResolvedValueOnce({ data: mockPosts })
render(<PostList />)
expect(await screen.findByText('Post 1')).toBeInTheDocument()
expect(await screen.findByText('Post 2')).toBeInTheDocument()
})
it('handles empty response', async () => {
axios.get.mockResolvedValueOnce({ data: [] })
render(<PostList />)
await waitFor(() => {
expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
})
})
})
Using Mock Service Worker (MSW):
npm install --save-dev msw
// mocks/handlers.js
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/users/:id', ({ params }) => {
const { id } = params
return HttpResponse.json({
id: parseInt(id),
name: 'John Doe',
email: '[email protected]'
})
}),
http.get('/api/posts', () => {
return HttpResponse.json([
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' }
])
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({
id: 3,
...body
}, { status: 201 })
})
]
// mocks/server.js
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// setupTests.js
import { server } from './mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
Test with MSW:
import { render, screen } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from './mocks/server'
import UserProfile from './UserProfile'
describe('UserProfile with MSW', () => {
it('displays user data', async () => {
render(<UserProfile userId={1} />)
expect(await screen.findByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('[email protected]')).toBeInTheDocument()
})
it('handles server errors', async () => {
server.use(
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 500 })
})
)
render(<UserProfile userId={1} />)
expect(await screen.findByText(/Error/i)).toBeInTheDocument()
})
it('handles different user data', async () => {
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.json({
id: 2,
name: 'Jane Smith',
email: '[email protected]'
})
})
)
render(<UserProfile userId={2} />)
expect(await screen.findByText('Jane Smith')).toBeInTheDocument()
})
})
Mocking POST requests:
// CreateUser.jsx
import { useState } from 'react'
export default function CreateUser() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [result, setResult] = useState(null)
const handleSubmit = async (e) => {
e.preventDefault()
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email })
})
const data = await response.json()
setResult(data)
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type='submit'>Create</button>
{result && <div>Created user: {result.name}</div>}
</form>
)
}
// CreateUser.test.jsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import CreateUser from './CreateUser'
global.fetch = jest.fn()
describe('CreateUser', () => {
it('creates user via API', async () => {
const user = userEvent.setup()
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, name: 'John', email: '[email protected]' })
})
render(<CreateUser />)
await user.type(screen.getByRole('textbox', { name: '' }), 'John')
await user.click(screen.getByRole('button', { name: 'Create' }))
expect(await screen.findByText('Created user: John')).toBeInTheDocument()
expect(fetch).toHaveBeenCalledWith('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John', email: '' })
})
})
})
Mocking custom hooks with API calls:
// useUsers.js
import { useState, useEffect } from 'react'
export function useUsers() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data)
setLoading(false)
})
}, [])
return { users, loading }
}
// useUsers.test.js
import { renderHook, waitFor } from '@testing-library/react'
import { useUsers } from './useUsers'
global.fetch = jest.fn()
describe('useUsers', () => {
it('fetches users', async () => {
const mockUsers = [{ id: 1, name: 'John' }]
fetch.mockResolvedValueOnce({
json: async () => mockUsers
})
const { result } = renderHook(() => useUsers())
expect(result.current.loading).toBe(true)
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.users).toEqual(mockUsers)
})
})
Mocking with different responses:
describe('Multiple API scenarios', () => {
it('handles success, then error', async () => {
fetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, name: 'John' })
})
.mockRejectedValueOnce(new Error('Server error'))
const { rerender } = render(<UserProfile userId={1} />)
expect(await screen.findByText('John')).toBeInTheDocument()
rerender(<UserProfile userId={2} />)
expect(await screen.findByText(/Error/i)).toBeInTheDocument()
})
})
Best Practice Note
MSW is recommended over Jest mocks—it intercepts requests at the network level, making tests more realistic. Use MSW handlers in setupTests.js for shared mocks across all tests. Override handlers per test using server.use() for specific scenarios. Mock at the network level (MSW) rather than module level (Jest) when possible. Clear mocks between tests with beforeEach to prevent interference. Test both success and error scenarios. This is how we mock API requests in CoreUI React tests—using MSW for realistic network mocking and Jest for simple cases, ensuring fast, reliable tests independent of external APIs.



