Advanced Testing Patterns
Testing Async Code and API Calls
Test async operations, loading states, error handling, and network calls reliably.
Async Testing in Vitest
Vitest handles async tests naturally with async/await:
typescript
it('fetches and returns user data', async () => {
const user = await fetchUser(123);
expect(user.name).toBe('Alice');
});
it('throws when user is not found', async () => {
await expect(fetchUser(-1)).rejects.toThrow('User not found');
});Testing Loading States in React
typescript
import { render, screen } from '@testing-library/react';
import { server } from '../test/msw-server';
import { http, HttpResponse } from 'msw';
import { UserProfile } from './UserProfile';
it('shows loading state then user data', async () => {
render(<UserProfile userId="123" />);
// Assert loading state appears immediately
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for data to load (findBy returns a promise)
const name = await screen.findByText('Alice Johnson');
expect(name).toBeInTheDocument();
// Loading indicator should be gone
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
it('shows error state when fetch fails', async () => {
// Override the handler for this test
server.use(
http.get('/api/users/:id', () =>
HttpResponse.json({ error: 'Not found' }, { status: 404 })
)
);
render(<UserProfile userId="123" />);
const error = await screen.findByText('User not found');
expect(error).toBeInTheDocument();
});Mock Service Worker (MSW)
MSW intercepts network requests at the network level, making tests more realistic than mocking fetch:
typescript
// src/test/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users/:id', ({ params }) => {
if (params.id === '123') {
return HttpResponse.json({ id: '123', name: 'Alice Johnson' });
}
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
}),
http.post('/api/posts', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 'new-post', ...body }, { status: 201 });
}),
];
// src/test/msw-server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/test/setup.ts
import { server } from './msw-server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());Testing Debounced Functions
typescript
import { vi } from 'vitest';
it('debounces search input', async () => {
vi.useFakeTimers();
const mockSearch = vi.fn();
render(<SearchInput onSearch={mockSearch} />);
const input = screen.getByRole('searchbox');
await userEvent.type(input, 'hello');
// Function should not have been called yet (debounced)
expect(mockSearch).not.toHaveBeenCalled();
// Advance time past debounce delay
vi.advanceTimersByTime(300);
expect(mockSearch).toHaveBeenCalledWith('hello');
expect(mockSearch).toHaveBeenCalledTimes(1); // Only once, not for each keystroke
vi.useRealTimers();
});Testing Streaming Responses (AI Features)
typescript
it('processes streaming AI response', async () => {
const chunks = ['Hello', ', ', 'world', '!'];
const stream = new ReadableStream({
start(controller) {
for (const chunk of chunks) {
controller.enqueue(new TextEncoder().encode(chunk));
}
controller.close();
},
});
global.fetch = vi.fn().mockResolvedValue(
new Response(stream, { headers: { 'Content-Type': 'text/stream' } })
);
const result = await processStreamingResponse('/api/chat', 'Hello AI');
expect(result).toBe('Hello, world!');
});Key Takeaways
- Use async/await directly in Vitest test functions — no special configuration needed
- findBy queries wait for async renders; queryBy returns null (not throws) when element is absent
- MSW intercepts at the network level — more realistic than mocking fetch, same test patterns
- vi.useFakeTimers() controls setTimeout/setInterval/Date.now() for testing time-dependent code
- Always reset fake timers after each test to prevent state leaking between tests
Example
typescript
// Testing async component with MSW
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server } from '../test/msw-server';
import { http, HttpResponse } from 'msw';
describe('PostList', () => {
it('fetches and displays posts', async () => {
server.use(
http.get('/api/posts', () =>
HttpResponse.json([
{ id: '1', title: 'First Post' },
{ id: '2', title: 'Second Post' },
])
)
);
render(<PostList />);
expect(screen.getByText('Loading posts...')).toBeInTheDocument();
await screen.findByText('First Post');
expect(screen.getByText('Second Post')).toBeInTheDocument();
expect(screen.queryByText('Loading posts...')).not.toBeInTheDocument();
});
});Try it yourself — TYPESCRIPT