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
- Use
userEvent.type()overfireEvent.change():userEventtriggers all the browser events that happen when a real user types (likekeydownandkeyup), ensuring a more reliable test. - Query by Accessibility Labels: Use
screen.getByLabelTextorscreen.getByRoleto locate elements. This ensures your tests fail if your HTML structure is not accessible to screen readers. - Assert on the DOM, Not State: Do not try to extract
the React state variable directly. Always assert on what the user sees
in the DOM, such as
expect(input).toHaveValue('value').