Testing Async Behaviour and the Network
AdvancedMock at the network boundary with MSW so your real fetching code runs. Then test the states everyone forgets: error, empty, and slow.
Overview
Most component tests involve data, and how you fake that data decides what the test is worth. Mocking your own API module means the fetch code, the error handling and the parsing are never exercised — the parts most likely to be wrong. Intercepting HTTP with MSW instead lets everything below the network run for real, and the same handlers work in tests, in Storybook and in the browser during development. On top of that, the discipline is to test the unhappy paths, because those are the ones nobody clicks through manually.
MSW
Handlers once, used by every test.
// src/test/server.js
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
export const server = setupServer(
http.get('/api/problems', () => HttpResponse.json([
{ id: 1, slug: 'two-sum', title: 'Two Sum', difficulty: 'easy' },
])),
http.post('/api/submissions', () => HttpResponse.json({ id: 9 }, { status: 201 })),
)
// setup.js
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers()) // per-test overrides do not leak
afterAll(() => server.close())
// onUnhandledRequest: 'error' is important — an unmocked request
// fails loudly instead of hanging until the test times out.
// Override for one test
server.use(
http.get('/api/problems', () => new HttpResponse(null, { status: 500 })),
)The States Nobody Tests
Error, empty and slow — where the real bugs are.
it('shows an error and allows retry', async () => {
server.use(http.get('/api/problems', () => new HttpResponse(null, { status: 500 })))
const { user } = renderWithProviders(<ProblemList />)
expect(await screen.findByRole('alert')).toHaveTextContent(/could not load/i)
server.use(http.get('/api/problems', () => HttpResponse.json([problem])))
await user.click(screen.getByRole('button', { name: /try again/i }))
expect(await screen.findByText('Two Sum')).toBeInTheDocument()
})
it('shows an empty state rather than a blank screen', async () => {
server.use(http.get('/api/problems', () => HttpResponse.json([])))
renderWithProviders(<ProblemList />)
expect(await screen.findByText(/no problems yet/i)).toBeInTheDocument()
})
it('disables submit while the request is in flight', async () => {
server.use(http.post('/api/submissions', async () => {
await delay(100)
return HttpResponse.json({ id: 1 })
}))
const { user } = renderWithProviders(<SubmitForm />)
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(screen.getByRole('button', { name: /submitting/i })).toBeDisabled()
})Async Assertions
The rules that eliminate flaky tests.
// findBy — waits for something to appear
expect(await screen.findByText('Two Sum')).toBeInTheDocument()
// waitFor — waits for an assertion to pass
await waitFor(() => expect(mockSubmit).toHaveBeenCalledTimes(1))
// waitForElementToBeRemoved — waits for disappearance
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'))
// getBy throws immediately — never use it for something async.
// This is the number one cause of flaky React tests:
expect(screen.getByText('Two Sum')).toBeInTheDocument() // too early
// Never use an arbitrary sleep. It is slow when the machine is fast
// and flaky when the machine is slow:
await new Promise(r => setTimeout(r, 500)) // wrong
// Asserting that something is ABSENT after async work needs care —
// wait for a positive signal first, then assert the absence:
expect(await screen.findByText('Loaded')).toBeInTheDocument()
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()Key Points to Remember
- 1Intercepting HTTP with MSW keeps your real fetching, error handling and parsing code under test
- 2Set onUnhandledRequest to error so an unmocked call fails loudly instead of timing out
- 3Reset handlers after each test so per-test overrides do not leak into the next
- 4Test error, retry, empty and in-flight states — the paths nobody exercises manually
- 5Use findBy and waitFor for anything asynchronous; getBy throws immediately and arbitrary sleeps cause flakiness
Interview Questions
Sign in to ask AriaWhy mock at the network level rather than mocking your API module?
What is the difference between getByText, findByText and waitFor?
How do you test that a submit button is disabled while a request is in flight?
Ask Aria about Testing Async Behaviour and the Network
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.