How to Mock Controlled Components in React
Mocking controlled components in React is a crucial technique for isolating your components during unit testing. This article explains how to mock a child controlled component using Jest and React Testing Library, allowing you to verify that your parent component correctly manages state and passes down the expected props without relying on the internal behavior of the child component.
To mock a controlled component, you need to replace the real
component with a simplified dummy version that accepts the same
props—specifically value and onChange—and
triggers them appropriately in your test environment.
Why Mock Controlled Components?
Controlled components rely on props to receive their current value and a callback function to signal changes. When testing a parent component, rendering complex child components can make tests slow and fragile. Mocking the child controlled component allows you to focus solely on the parent component’s logic, ensuring it handles state changes correctly.
Step-by-Step Implementation
Consider a parent component that contains a complex custom controlled input:
// ParentComponent.jsx
import React, { useState } from 'react';
import CustomInput from './CustomInput';
export default function ParentComponent() {
const [text, setText] = useState('');
return (
<div>
<h1>Parent Form</h1>
<CustomInput value={text} onChange={setText} />
<p data-testid="display-text">Value: {text}</p>
</div>
);
}To test ParentComponent without rendering the actual
CustomInput, you can mock it at the top of your test
file.
Writing the Test with a Mocked Component
Use jest.mock() to replace the child component with a
simple HTML input. This mock captures the value and
onChange props and maps them to a basic input element.
// ParentComponent.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ParentComponent from './ParentComponent';
// Mock the child controlled component
jest.mock('./CustomInput', () => {
return function MockedCustomInput({ value, onChange }) {
return (
<input
data-testid="mocked-input"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
);
};
});
describe('ParentComponent', () => {
it('updates state when the mocked controlled component changes', () => {
render(<ParentComponent />);
const input = screen.getByTestId('mocked-input');
const display = screen.getByTestId('display-text');
// Assert initial state
expect(display.textContent).toBe('Value: ');
// Simulate user typing in the mocked component
fireEvent.change(input, { target: { value: 'Hello World' } });
// Assert that parent state updated correctly
expect(display.textContent).toBe('Value: Hello World');
});
});Key Takeaways
- Replicate the Interface: Ensure your mocked component accepts the same props as the original. For controlled components, this always includes the state value and the state-updating function.
- Use Test IDs: Assign a
data-testidto the mocked element so you can easily query it in your test assertions. - Trigger Callbacks: Map domestic HTML events (like
onChange) in your mock to trigger the custom callback props passed down by the parent.