Enzyme React Testing: Shallow, Mount, and Migration to Testing Library

Enzyme React Testing: Shallow, Mount, and Migration to Testing Library

Enzyme was the dominant React testing library for years, giving developers the ability to render components in isolation and traverse the output as if it were a DOM tree. While React Testing Library has largely taken over for new projects, millions of lines of Enzyme tests still exist in production codebases. Understanding Enzyme — and knowing when and how to migrate away from it — remains a practical skill.

This guide covers Enzyme from setup through real-world patterns, including the shallow vs mount debate, event simulation, snapshot serialization, and a pragmatic migration strategy.

Installation and Adapter Setup

Enzyme requires an adapter that matches your React version. As of React 17+, the official adapter is no longer maintained, so you'll use the community adapter:

# React 16
npm install --save-dev enzyme enzyme-adapter-react-16

# React 17 (community adapter)
npm install --save-dev enzyme @wojtekmaj/enzyme-adapter-react-17

# React 18 (community adapter)
npm install --save-dev enzyme @wojtekmaj/enzyme-adapter-react-18

Configure the adapter in your test setup file:

// src/setupTests.ts
import Enzyme from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';

Enzyme.configure({ adapter: new Adapter() });

Reference this file in Jest config:

{
  "jest": {
    "setupFilesAfterFramework": ["./src/setupTests.ts"]
  }
}

Shallow Rendering

shallow renders only the component itself, not its children. Child components appear as their element tags rather than fully rendered output. This makes shallow rendering fast and isolated — changes in a child component won't break a parent's test.

import { shallow } from 'enzyme';
import UserCard from './UserCard';

it('renders the user name', () => {
  const wrapper = shallow(<UserCard name="Alice" role="admin" />);

  expect(wrapper.find('.user-name').text()).toBe('Alice');
  expect(wrapper.find('Avatar')).toHaveLength(1); // Avatar not rendered, just present
});

Shallow rendering is appropriate when:

  • You want to test a component in complete isolation
  • Children are simple or covered by their own tests
  • You're testing conditional rendering logic

The downside is that shallow tests can pass even when the component is actually broken at the integration level, because children are never exercised.

Mount Rendering

mount performs full DOM rendering, including all child components. It requires a DOM environment (jsdom in Jest).

import { mount } from 'enzyme';
import LoginForm from './LoginForm';

it('submits the form with email and password', () => {
  const onSubmit = jest.fn();
  const wrapper = mount(<LoginForm onSubmit={onSubmit} />);

  wrapper.find('input[name="email"]').simulate('change', {
    target: { value: 'alice@example.com' },
  });
  wrapper.find('input[name="password"]').simulate('change', {
    target: { value: 'secret123' },
  });
  wrapper.find('button[type="submit"]').simulate('click');

  expect(onSubmit).toHaveBeenCalledWith({
    email: 'alice@example.com',
    password: 'secret123',
  });
});

Mount tests are slower but more realistic. They catch integration issues that shallow tests miss, such as context propagation failures or child component bugs that affect parent behavior.

Traversal and Selectors

Enzyme's API is modeled after jQuery and supports CSS-style selectors, component names, display names, and prop-based selectors:

wrapper.find('.btn-primary')          // CSS class
wrapper.find('Button')                // component display name
wrapper.find({ 'data-testid': 'submit' }) // prop selector
wrapper.find('ul li').at(2)           // third list item
wrapper.findWhere(n => n.text() === 'Click me') // custom predicate

// Traversal
wrapper.children()
wrapper.parent()
wrapper.closest('form')

// State and props
wrapper.state('isOpen')
wrapper.prop('onClick')
wrapper.instance()                    // class component instance

Simulating Events

simulate triggers synthetic React events. It does not simulate real DOM events — it directly calls the event handler prop:

// Simulating change
wrapper.find('input').simulate('change', { target: { value: 'hello' } });

// Simulating keyboard events
wrapper.find('input').simulate('keyDown', { key: 'Enter', keyCode: 13 });

// Simulating focus/blur
wrapper.find('input').simulate('focus');
wrapper.find('input').simulate('blur');

A common gotcha: simulate('click') calls the onClick prop directly. If your handler calls event.preventDefault(), you need to pass a mock event object:

const mockEvent = { preventDefault: jest.fn() };
wrapper.find('a').simulate('click', mockEvent);
expect(mockEvent.preventDefault).toHaveBeenCalled();

Snapshots with enzyme-to-json

Enzyme wrappers serialize poorly by default. The enzyme-to-json package converts them to a readable format for Jest snapshots:

npm install --save-dev enzyme-to-json
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Button from './Button';

it('matches snapshot', () => {
  const wrapper = shallow(<Button label="Submit" primary />);
  expect(toJson(wrapper)).toMatchSnapshot();
});

Or configure it globally so all Enzyme wrappers auto-serialize:

{
  "jest": {
    "snapshotSerializers": ["enzyme-to-json/serializer"]
  }
}

Testing Hooks and Functional Components

Enzyme's support for hooks improved significantly but remains incomplete compared to React Testing Library. For testing custom hooks in isolation, prefer @testing-library/react-hooks. For functional components with hooks in Enzyme, mount works but shallow has known issues with useEffect and context:

import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import Counter from './Counter';

it('increments on button click', () => {
  const wrapper = mount(<Counter initialCount={0} />);

  act(() => {
    wrapper.find('button').simulate('click');
  });
  wrapper.update(); // force Enzyme to re-read from the DOM

  expect(wrapper.find('.count').text()).toBe('1');
});

The wrapper.update() call is necessary after act() to sync Enzyme's internal state with React's render output.

Why Teams Migrate to React Testing Library

React Testing Library (RTL) takes a fundamentally different philosophy: test what the user sees, not how the component is implemented. This means:

  • No wrapper.state() — tests don't inspect internal state
  • No component traversal by display name — tests query by accessible role, label, or text
  • No shallow rendering — everything renders fully, like a real browser
  • Built-in act() wrapping — async updates work correctly out of the box

The practical result is tests that are less brittle. When you refactor a component's internal structure, RTL tests keep passing as long as the user-visible behavior is unchanged. Enzyme tests often break because a selector like find('InternalHelper') no longer matches.

Migration Strategy

A full rewrite is rarely practical. The pragmatic approach:

  1. Stop writing new Enzyme tests. Use RTL for all new components.
  2. Migrate on touch. When you modify a component, migrate its Enzyme tests at the same time.
  3. Use @testing-library/react as a drop-in. The setup is nearly identical; the query API is different but learnable in a day.
  4. Prioritize high-churn components. Components that change frequently benefit most from the migration.
// Enzyme (before)
const wrapper = shallow(<SearchInput onSearch={fn} />);
wrapper.find('input').simulate('change', { target: { value: 'query' } });
wrapper.find('.search-btn').simulate('click');
expect(fn).toHaveBeenCalledWith('query');

// React Testing Library (after)
render(<SearchInput onSearch={fn} />);
userEvent.type(screen.getByRole('textbox'), 'query');
userEvent.click(screen.getByRole('button', { name: /search/i }));
expect(fn).toHaveBeenCalledWith('query');

The logic is identical; only the API changes.

Summary

Enzyme remains a capable tool for React component testing, especially in codebases that predate React Testing Library. Shallow rendering gives fast, isolated unit tests; mount rendering gives integration confidence. The simulate API covers most interaction patterns, and enzyme-to-json makes snapshot tests readable. For new projects or active refactoring efforts, migrating to React Testing Library pays off quickly in reduced test brittleness and better alignment with how users actually interact with your UI.

Read more

Start now free