Building Your Learning Module...
Getting things ready for you!
Find videos you like?
Save to resource drawer for future reference!
Modern React apps are full of asynchronous operations: fetching data from APIs, waiting for user actions, handling loading states, and more. Testing these scenarios requires special techniques to wait for async operations to complete.
React Testing Library provides powerful tools like waitFor() and findBy* queries to handle these async scenarios!
Test that loading indicators appear and disappear correctly.
Verify data fetching, success, and error handling work correctly.
Test components with setTimeout, debounce, and throttle.
This UserProfile component fetches data asynchronously. Watch it transition from loading → success state. Perfect for testing async behavior!
<div id="root"></div>Loading preview...
React Testing Library gives you multiple tools to handle async operations. Choose the right one for your situation:
Wait until a condition becomes true. Most flexible option!
Query + wait combined. Returns promise that resolves when element appears.
Wait for an element to disappear from the DOM.
Verify loading appears then disappears when data loads
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
// Mock fetch API
global.fetch = jest.fn();
describe('UserProfile - Loading State', () => {
beforeEach(() => {
fetch.mockClear();
});
test('shows loading state initially', () => {
fetch.mockResolvedValue({
json: async () => ({ name: 'Sarah', email: 'sarah@test.com' })
});
render(<UserProfile userId="123" />);
// Loading should appear immediately
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
test('hides loading after data loads', async () => {
fetch.mockResolvedValue({
json: async () => ({ name: 'Sarah', email: 'sarah@test.com' })
});
render(<UserProfile userId="123" />);
// Wait for loading to disappear
await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
});
test('shows user data after loading completes', async () => {
const userData = { name: 'Sarah Johnson', email: 'sarah@test.com' };
fetch.mockResolvedValue({
json: async () => userData
});
render(<UserProfile userId="123" />);
// Wait for and verify user data appears
expect(await screen.findByText('Sarah Johnson')).toBeInTheDocument();
expect(await screen.findByText(/sarah@test.com/i)).toBeInTheDocument();
});
});Mock API calls and verify correct data handling
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
global.fetch = jest.fn();
describe('UserList - API Calls', () => {
beforeEach(() => {
fetch.mockClear();
});
test('fetches and displays users', async () => {
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
fetch.mockResolvedValue({
json: async () => users
});
render(<UserList />);
// Wait for users to appear
expect(await screen.findByText('Alice')).toBeInTheDocument();
expect(await screen.findByText('Bob')).toBeInTheDocument();
// Verify fetch was called correctly
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith('/api/users');
});
test('calls API with correct parameters', async () => {
fetch.mockResolvedValue({
json: async () => []
});
render(<UserList filter="active" />);
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith('/api/users?status=active');
});
});
test('refetches when props change', async () => {
fetch.mockResolvedValue({
json: async () => []
});
const { rerender } = render(<UserList filter="active" />);
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
// Change props and verify new fetch
rerender(<UserList filter="inactive" />);
await waitFor(() => {
expect(fetch).toHaveBeenCalledTimes(2);
expect(fetch).toHaveBeenLastCalledWith('/api/users?status=inactive');
});
});
});Verify error handling and error messages display correctly
import { render, screen, waitFor } from '@testing-library/react';
import DataFetcher from './DataFetcher';
global.fetch = jest.fn();
describe('DataFetcher - Error Handling', () => {
beforeEach(() => {
fetch.mockClear();
// Suppress console errors in tests
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
console.error.mockRestore();
});
test('displays error message when fetch fails', async () => {
fetch.mockRejectedValue(new Error('Network error'));
render(<DataFetcher />);
// Wait for error message to appear
expect(await screen.findByText(/network error/i)).toBeInTheDocument();
// Loading should be gone
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
test('displays error for 404 response', async () => {
fetch.mockResolvedValue({
ok: false,
status: 404,
json: async () => ({ message: 'Not found' })
});
render(<DataFetcher url="/api/users/999" />);
expect(await screen.findByText(/not found/i)).toBeInTheDocument();
});
test('allows retry after error', async () => {
fetch.mockRejectedValueOnce(new Error('Failed'))
.mockResolvedValueOnce({
json: async () => ({ data: 'Success' })
});
render(<DataFetcher />);
// Wait for error
const retryButton = await screen.findByRole('button', { name: /retry/i });
// Click retry
fireEvent.click(retryButton);
// Wait for success
expect(await screen.findByText('Success')).toBeInTheDocument();
expect(fetch).toHaveBeenCalledTimes(2);
});
});Handle setTimeout, setInterval, and debounce in tests
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchBox from './SearchBox';
// Mock timer functions
jest.useFakeTimers();
describe('SearchBox - Timers', () => {
afterEach(() => {
jest.clearAllTimers();
});
test('debounces search input', async () => {
const handleSearch = jest.fn();
const user = userEvent.setup({ delay: null });
render(<SearchBox onSearch={handleSearch} debounceMs={500} />);
const input = screen.getByRole('textbox');
// Type quickly (should be debounced)
await user.type(input, 'React');
// Search should not be called yet
expect(handleSearch).not.toHaveBeenCalled();
// Fast-forward time
jest.advanceTimersByTime(500);
// Now search should be called
await waitFor(() => {
expect(handleSearch).toHaveBeenCalledWith('React');
expect(handleSearch).toHaveBeenCalledTimes(1);
});
});
test('shows notification after delay', async () => {
render(<Notification message="Saved!" duration={3000} />);
// Notification should be visible
expect(screen.getByText('Saved!')).toBeInTheDocument();
// Fast-forward time
jest.advanceTimersByTime(3000);
// Wait for notification to disappear
await waitFor(() => {
expect(screen.queryByText('Saved!')).not.toBeInTheDocument();
});
});
test('auto-saves with interval', async () => {
const handleSave = jest.fn();
render(<AutoSaveEditor onSave={handleSave} interval={5000} />);
// No saves yet
expect(handleSave).not.toHaveBeenCalled();
// Fast-forward 5 seconds
jest.advanceTimersByTime(5000);
expect(handleSave).toHaveBeenCalledTimes(1);
// Fast-forward another 5 seconds
jest.advanceTimersByTime(5000);
expect(handleSave).toHaveBeenCalledTimes(2);
});
});Need to wait for an element to appear?
✅ Use findBy* queries
Need to wait for an element to disappear?
✅ Use waitForElementToBeRemoved()
Need to wait for any custom condition?
✅ Use waitFor(() => expect(...))
Testing setTimeout or setInterval?
✅ Use jest.useFakeTimers() and jest.advanceTimersByTime()
Use findBy* instead of wrapping getBy* in waitFor.
Always await async queries and waitFor. Forgetting causes tests to fail unexpectedly!
Always mock fetch, axios, or other external APIs. Don't make real network requests in tests!
Test loading state, success state, and error state. Cover the full async lifecycle!
Wait for elements to appear. Returns a promise that resolves when found.
Wait for any custom condition. Most flexible async testing tool.
Verify loading indicators appear and disappear correctly.
Always mock fetch and external APIs. No real network calls!
Test error states and verify error messages display correctly.
Use jest.useFakeTimers() for setTimeout and debounce testing.
You now know how to test all async scenarios! Final topic: