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

  1. 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.
  2. Use Test IDs: Assign a data-testid to the mocked element so you can easily query it in your test assertions.
  3. Trigger Callbacks: Map domestic HTML events (like onChange) in your mock to trigger the custom callback props passed down by the parent.