How to Test Client-Side Rendering in React

Testing Client-Side Rendering (CSR) in React is essential for ensuring your application’s user interface behaves correctly as state, props, and browser-based events change. This article provides a straightforward guide on how to test CSR in React, covering the industry-standard testing stack, how to simulate real user interactions, and how to handle common CSR scenarios like asynchronous data fetching and client-side routing.

The Core Testing Stack: Jest and React Testing Library

To effectively test client-side rendered React applications, the industry standard is to use Jest (or Vitest) as the test runner and React Testing Library (RTL) for rendering components.

React Testing Library is specifically designed for testing CSR because it encourages testing user behavior rather than implementation details. It renders your components into a virtual DOM (using jsdom), allowing you to query elements just like a real user would.

import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';

test('renders the welcome message', () => {
  render(<MyComponent />);
  const linkElement = screen.getByText(/welcome to our app/i);
  expect(linkElement).toBeInTheDocument();
});

Simulating User Interactions

Because CSR relies heavily on the browser to handle events (like clicks, form submissions, and keyboard inputs), your tests must accurately simulate these interactions.

Using @testing-library/user-event is the recommended way to trigger browser events, as it mimics real-world browser behavior more closely than the legacy fireEvent utility.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('increments counter on button click', async () => {
  render(<Counter />);
  const button = screen.getByRole('button', { name: /increment/i });
  
  await userEvent.click(button);
  
  const countValue = screen.getByText(/count: 1/i);
  expect(countValue).toBeInTheDocument();
});

Handling Asynchronous Data Fetching

In client-side rendering, components often mount in a loading state and then fetch data from an API to populate the UI. Testing this flow requires mocking the API calls and waiting for the DOM to update.

  1. Mock the API network request: Use tools like Mock Service Worker (MSW) or Jest mocks to intercept API calls.
  2. Use findBy queries: RTL’s findBy queries return a promise and automatically wait for the element to appear in the DOM.
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';

// Example using a global fetch mock
beforeEach(() => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ name: 'John Doe' }),
    })
  );
});

test('fetches and displays user data', async () => {
  render(<UserProfile />);
  
  // Wait for the loader to disappear and the user name to render
  const profileName = await screen.findByText('John Doe');
  expect(profileName).toBeInTheDocument();
});

Testing Client-Side Routing

If your React application uses client-side routing (such as React Router), you must wrap your components in a router provider during testing.

Using MemoryRouter allows you to manually set the initial history entries to test how components render at specific URL paths.

import { render, screen } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import Dashboard from './Dashboard';

test('renders dashboard when navigating to /dashboard', () => {
  render(
    <MemoryRouter initialEntries={['/dashboard']}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </MemoryRouter>
  );

  expect(screen.getByRole('heading', { name: /dashboard/i })).toBeInTheDocument();
});