Mocking Uncontrolled Components in React

Mocking uncontrolled components in React is a crucial technique for isolating components during unit testing. Since uncontrolled components rely on the DOM and React refs to manage their own internal state rather than props, testing their integration requires specific mocking strategies. This article explains how to mock these components using Jest and React Testing Library, focusing on replacing the component with a mock implementation and simulating DOM interactions.

Why Mock Uncontrolled Components?

Uncontrolled components, such as a standard HTML <input type="file" /> or third-party rich text editors, manage their own state internally. In unit tests, you often want to test how the parent component reacts to changes in these children without dealing with their complex internal DOM manipulations. Mocking these components simplifies your test suite, rendering a predictable mock instead of the actual uncontrolled element.

Method 1: Mocking the Component with Jest

The most robust way to handle an uncontrolled child component is to mock the entire module using jest.mock(). By replacing the uncontrolled component with a simple controlled mock, you can easily trigger events and verify if the parent component behaves correctly.

Assume you have an uncontrolled component named UncontrolledForm:

// UncontrolledForm.jsx
import React, { useRef } from 'react';

export default function UncontrolledForm({ onSubmit }) {
  const inputRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={inputRef} />
      <button type="submit">Submit</button>
    </form>
  );
}

You can mock this component in your test file so it behaves like a controlled component, making it easy to pass values:

// ParentComponent.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ParentComponent from './ParentComponent';

// Mock the uncontrolled component
jest.mock('./UncontrolledForm', () => {
  return function MockUncontrolledForm({ onSubmit }) {
    return (
      <div data-testid="mock-form">
        <input 
          data-testid="mock-input" 
          onChange={(e) => onSubmit(e.target.value)} 
        />
      </div>
    );
  };
});

test('should receive data from mocked uncontrolled component', () => {
  const handleData = jest.fn();
  render(<ParentComponent onDataReceived={handleData} />);

  const mockInput = screen.getByTestId('mock-input');
  fireEvent.change(mockInput, { target: { value: 'Test Value' } });

  expect(handleData).toHaveBeenCalledWith('Test Value');
});

Method 2: Mocking React.useRef

If you cannot mock the entire component and must test the file containing the ref directly, you can spy on and mock React.useRef. This allows you to control the .current property that the uncontrolled component relies on.

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import UncontrolledForm from './UncontrolledForm';

test('should submit the mocked ref value', () => {
  const mockSubmit = jest.fn();
  
  // Spy on useRef and return a custom current object
  const refSpy = jest.spyOn(React, 'useRef').mockReturnValue({
    current: { value: 'Mocked Input Value' }
  });

  const { getByRole } = render(<UncontrolledForm onSubmit={mockSubmit} />);
  
  const submitButton = getByRole('button', { name: /submit/i });
  fireEvent.click(submitButton);

  expect(mockSubmit).toHaveBeenCalledWith('Mocked Input Value');
  
  refSpy.mockRestore();
});

Using these two approaches, you can easily bypass the limitations of testing uncontrolled elements, ensuring your React tests remain fast, reliable, and isolated.