How to Test Controlled Components in React

Testing controlled components in React is essential for ensuring that user inputs correctly update the component’s state and render the expected UI. This article provides a straightforward guide on how to test these components using Jest and React Testing Library, focusing on simulating user interactions and asserting that state changes occur as expected.

Understanding the Test Strategy

Controlled components rely on React state to manage their values, typically updating via an onChange event handler. To test them effectively, you should avoid testing the internal state directly. Instead, test the component from the user’s perspective: 1. Render the component. 2. Query the input element. 3. Simulate a user typing into the input. 4. Assert that the input displays the correct value and triggers any associated callback functions.

The Component to Test

Here is a standard controlled input component in React:

import React, { useState } from 'react';

export function UsernameForm({ onSubmit }) {
  const [username, setUsername] = useState('');

  const handleSubmit = (event) => {
    event.preventDefault();
    onSubmit(username);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Username</label>
      <input
        id="username"
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Writing the Test with React Testing Library

To test this component, use @testing-library/react and @testing-library/user-event. The user-event library is preferred over fireEvent because it more accurately mimics real browser interactions.

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

test('updates input value on typing and submits the correct data', async () => {
  const handleSubmit = jest.fn();
  render(<UsernameForm onSubmit={handleSubmit} />);

  // 1. Locate the input and the button
  const input = screen.getByLabelText(/username/i);
  const button = screen.getByRole('button', { name: /submit/i });

  // 2. Assert initial empty state
  expect(input).toHaveValue('');

  // 3. Simulate user typing
  await userEvent.type(input, 'developer123');

  // 4. Assert that the input value updated correctly
  expect(input).toHaveValue('developer123');

  // 5. Simulate form submission
  await userEvent.click(button);

  // 6. Assert that the submit callback received the correct state value
  expect(handleSubmit).toHaveBeenCalledTimes(1);
  expect(handleSubmit).toHaveBeenCalledWith('developer123');
});

Key Best Practices