Catch Visual UI Bugs with SmartUI Visual Testing

Catch Layout, CSS, and Rendering Bugs across Browsers and Real Devices. Visual AI Filters Out Pixel Noise.

Try SmartUI free
Web DevelopmentJavaScript

React Testing Library vs Enzyme: Key Differences and How to Migrate

Compare React Testing Library vs Enzyme on rendering, state access, events and React 19 support, with npm data from November 2026 and seven migration steps.

Published on:

A pull request upgrades a React app to 19.3.1, and the test job stops at npm install: enzyme-adapter-react-16, the newest official Enzyme adapter, accepts only React 16. For a team with hundreds of Enzyme tests, that error turns the React Testing Library vs Enzyme question into a migration plan.

This guide compares the two libraries on rendering, state access, events and React support, with every version and date read from npm and GitHub on November 11, 2026. It then moves a sample Enzyme test file to React Testing Library one API at a time, using the npm, ESLint and inventory output from the runs behind it, and shows where TestMu AI covers what neither library can see in jsdom.

Overview

Use React Testing Library in 2026. Version 16.3.3 supports React 18 and 19 and tests the DOM a user sees, while Enzyme's last release, 3.11.0 from December 2019, has official adapters only up to React 16. Teams with Enzyme suites should move them to React Testing Library file by file.

What Is the Difference Between React Testing Library and Enzyme?

  • React Testing Library: renders the full component tree into the DOM and finds elements by role, label and text, so tests check what a user sees and survive refactors. Shallow rendering: no. Official React 19 support: yes, since 16.1.0.
  • Enzyme: renders with shallow(), mount() or a static render() and lets tests read and set props, state and instance methods directly. Shallow rendering: yes. Official React 18 or 19 adapter: no.
  • user-event: the interaction library React Testing Library pairs with. It fires the full sequence of browser events for a click or keystroke, while Enzyme's simulate() on a shallow wrapper calls the matching prop handler directly.

Does Enzyme Work with React 18 or React 19?

Not officially: Enzyme's official adapters end at React 16. Community adapters exist for React 18 (0.8.0, April 2024) and React 19 (a fork published once, in October 2025), and both READMEs answer "Should you count on it?" with "Probably not." React Testing Library has supported React 19 since version 16.1.0.

What Happens to Enzyme Snapshot Tests After the Migration?

When an Enzyme suite moves to React Testing Library, snapshots serialized with enzyme-to-json stop matching once the serializer is removed. Re-record the ones worth keeping with React Testing Library's asFragment(), or move visual checks to screenshots: TestMu AI's SmartUI captures every Storybook story in real browsers and compares it with an approved baseline.

React Testing Library vs Enzyme: Comparison Table (November 2026)

The two libraries differ most on how a test reaches the component and on which React versions they can run against. Every version, date and count in the table was read from the npm registry and GitHub on November 11, 2026.

CriterionReact Testing LibraryEnzyme
Testing approachFinds elements by role, label and text in the rendered DOM, the way a user finds themTraverses the React component tree with selectors, component names and props
RenderingOne render() that mounts the full tree into a container in document.bodyshallow() leaves child components unrendered, mount() renders to a DOM, and render() returns static HTML parsed by Cheerio
State and props accessNone by design; tests assert on what state and props renderstate(), setState(), props(), setProps() and instance()
User eventsuser-event fires the browser events of a full interaction; fireEvent dispatches one eventsimulate(); on a shallow wrapper it calls the matching prop handler, and events do not propagate
React 18 supportYes, since 13.0.0 (March 31, 2022)No official adapter; unofficial @cfaester/enzyme-adapter-react-18 0.8.0 (April 29, 2024)
React 19 supportYes, since 16.1.0 (December 5, 2024)No official adapter; unofficial @belzile/enzyme-adapter-react-19 19.0.0 (October 17, 2025)
Latest release16.3.3, August 27, 20263.11.0, December 20, 2019
Repository statusActive; latest commit August 27, 2026Not archived; latest commit October 22, 2025, which pinned a test dependency
npm downloads, November 4 to 10, 202692,793,5571,283,722

Sources: the npm pages for enzyme and @testing-library/react and the GitHub repositories of Enzyme and React Testing Library, all read on November 11, 2026. The download figures are npm's counts for November 4 to 10, 2026.

npm counted 1,283,722 downloads of enzyme in that week and 92,793,557 of @testing-library/react in the same week, a ratio of about 1 to 72, although no official Enzyme adapter accepts a React release newer than 16.x. The adapter details are in the React 18 and React 19 section below.

How Do React Testing Library and Enzyme Differ?

Each difference in the table changes what a test can see and what breaks it. The examples use CartSummary, a class component with a coupon toggle and a checkout button, whose full code opens the migration steps.

Testing Philosophy: User Behavior vs Component Internals

React Testing Library is built on one of its guiding principles: "The more your tests resemble the way your software is used, the more confidence they can give you." Its queries find a button by its role and name and a field by its label, so a test fails when something a user relies on changes and survives a refactor that leaves the screen the same.

Enzyme's README describes its API as "mimicking jQuery's API for DOM manipulation and traversal", applied to the React tree. A test can reach any prop, state value or method, which is convenient until a refactor moves that detail: an assertion on wrapper.state('couponOpen') fails the day the flag moves into a hook, though nothing on screen changed.

Shallow Rendering vs Full Rendering

Enzyme's shallow rendering API renders one component and leaves its child components unrendered, which its docs present as a way "to ensure that your tests aren't indirectly asserting on behavior of child components". React Testing Library has no shallow mode: its render() API mounts the full tree into a container appended to document.body.

React's own team discourages shallow rendering. React 19 removed react-test-renderer/shallow, and the React 19 upgrade guide warns that "Shallow rendering depends on React internals and can block you from future upgrades" while telling teams that still need it to install react-shallow-renderer directly. In a project pinned to React 19.3.1, that command fails with npm 11.8.0, because the package's latest release declares peer support up to React 18:

$ npm install react-shallow-renderer --save-dev --dry-run
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: cart-app@1.0.0
npm error Found: react@19.3.1
npm error node_modules/react
npm error   react@"19.3.1" from the root project
npm error
npm error Could not resolve dependency:
npm error peer react@"^16.0.0 || ^17.0.0 || ^18.0.0" from react-shallow-renderer@16.15.0
npm error node_modules/react-shallow-renderer
npm error   dev react-shallow-renderer@"*" from the root project

The run used --dry-run, so npm resolved the tree without installing anything. A React Testing Library suite never meets this error, because its only renderer is react-dom.

State and Props Access in Enzyme vs React Testing Library

Enzyme wrappers expose a component's internals. Its API documentation says state() returns this.state of the root component instance, setState() changes it, props() and setProps() read and replace the root's props, and instance() hands the test the component object so it can call methods directly. A test can put CartSummary into any state without touching the UI.

React Testing Library has no equivalent, and its guiding principles say its utilities should work with "DOM nodes rather than component instances". The test drives the component the way a user does and checks the result: instead of reading couponOpen, it clicks Add coupon and checks that the Coupon code field and a Hide coupon button appear.

Enzyme simulate() vs user-event

Enzyme's documentation for simulate() on a shallow wrapper is direct about it: "Even though the name would imply this simulates an actual event, .simulate() will in fact target the component's prop based on the event you give it." A simulated click calls the onClick prop, events do not propagate, and a handler that calls event.preventDefault() needs a mock event object passed in.

The user-event docs say the library "simulates full interactions, which may fire multiple events and do additional checks along the way", and that it factors in that a browser "wouldn't let a user click a hidden element or type in a disabled text box". fireEvent, which ships with React Testing Library, dispatches a single DOM event for the cases where one event is all a test needs.

Enzyme Support for React 18 and React 19

Enzyme reaches React through an adapter, and the official list in Enzyme's README goes no further than enzyme-adapter-react-16. Every adapter for a later React version is a community package, and npm shows how far each one got:

  • enzyme-adapter-react-16 - the official adapter, for React 16 only (peer range ^16.0.0-0); npm lists 1.15.8, published on February 10, 2024, as its latest release, and it depends on react-test-renderer, which React's docs mark as deprecated.
  • @wojtekmaj/enzyme-adapter-react-17 - unofficial; 0.8.0 was published on November 9, 2022, and its GitHub repository is now archived.
  • @cfaester/enzyme-adapter-react-18 - unofficial; 0.8.0 was published on April 29, 2024, and its README on npm calls it "A very unofficial adapter for React 18" and answers "Should you count on it?" with "Probably not."
  • @belzile/enzyme-adapter-react-19 - an unofficial fork published once, as 19.0.0 on October 17, 2025, whose npm README still describes an adapter "for React 18" and gives the same answer.

npm does not block the React 18 adapter on React 19, because it declares react >=18. A dry run in the same React 19.3.1 project printed one warning and resolved the tree:

$ npm install --dry-run -D enzyme@3.11.0 @cfaester/enzyme-adapter-react-18@0.8.0
npm warn ERESOLVE overriding peer dependency

added 146 packages in 2s

The 146 lines that list each package are left out above. npm's debug log names the overridden dependency: react-shallow-renderer 16.15.0, which the adapter depends on and which declares peer support only up to React 18, so a clean install says nothing about whether shallow rendering works on React 19. The same dry run with @testing-library/react 16.3.3, @testing-library/dom 10.4.2, @testing-library/user-event 14.6.9 and @testing-library/jest-dom 7.0.1 added 28 packages and printed no warning.

Layout Bugs That React Testing Library and Enzyme Both Miss

Neither library runs a browser. React Testing Library and Enzyme's mount() render into a DOM implementation such as jsdom, and the jsdom README lists layout, "the ability to calculate where elements will be visually laid out as a result of CSS", as outside its scope.

A sticky header covering a button or a Safari-only rendering bug needs React end-to-end testing in real browsers, which TestMu AI's automation cloud runs for Playwright, Cypress and Selenium suites across 3,000+ browser and OS combinations in parallel.

Test across 3000+ browser and OS environments with TestMu AI

How to Migrate from Enzyme to React Testing Library

Migrate file by file while both libraries run in the same suite; Testing Library's own migration guide recommends the same incremental path. The steps use CartSummary, a class component of the kind many Enzyme suites still test:

// src/CartSummary.jsx
import { Component } from 'react';

export default class CartSummary extends Component {
  state = { couponOpen: false };

  toggleCoupon = () => {
    this.setState((prev) => ({ couponOpen: !prev.couponOpen }));
  };

  render() {
    const { items, onCheckout } = this.props;
    const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);

    return (
      <section aria-label="Cart summary">
        <p className="total">Total: {total.toFixed(2)}</p>
        <button className="coupon-toggle" onClick={this.toggleCoupon}>
          {this.state.couponOpen ? 'Hide coupon' : 'Add coupon'}
        </button>
        {this.state.couponOpen && (
          <label>Coupon code <input name="coupon" /></label>
        )}
        <button
          className="checkout"
          disabled={items.length === 0}
          onClick={() => onCheckout(total)}
        >
          Checkout
        </button>
      </section>
    );
  }
}

Its Enzyme tests use the calls that create most of the migration work: shallow(), mount(), find(), simulate(), state() and setProps().

// src/CartSummary.enzyme.test.jsx (before)
import { shallow, mount } from 'enzyme';
import CartSummary from './CartSummary';

const items = [{ id: 1, price: 12.5, qty: 2 }];

it('shows the total', () => {
  const wrapper = shallow(<CartSummary items={items} onCheckout={() => {}} />);
  expect(wrapper.find('.total').text()).toBe('Total: 25.00');
});

it('opens the coupon field', () => {
  const wrapper = shallow(<CartSummary items={items} onCheckout={() => {}} />);
  wrapper.find('.coupon-toggle').simulate('click');
  expect(wrapper.state('couponOpen')).toBe(true);
});

it('passes the total to onCheckout', () => {
  const onCheckout = jest.fn();
  const wrapper = mount(<CartSummary items={items} onCheckout={onCheckout} />);
  wrapper.find('button.checkout').simulate('click');
  expect(onCheckout).toHaveBeenCalledWith(25);
});

it('disables checkout when the cart empties', () => {
  const wrapper = mount(<CartSummary items={items} onCheckout={() => {}} />);
  wrapper.setProps({ items: [] });
  expect(wrapper.find('button.checkout').prop('disabled')).toBe(true);
});

How the examples were checked: npm resolution ran as dry runs with npm 11.8.0 in projects pinned to React 19.3.1 and React 17.0.2 on November 11, 2026. CartSummary was rendered on React 19.2.4 in jsdom 29.1.1 to confirm every value the tests expect, and the inventory script and the ESLint rule ran as shown. The Enzyme and React Testing Library calls follow each library's current documentation.

Step 1: Inventory the Enzyme APIs in Your Test Files

Count what you are replacing before you change anything. This Node.js script walks a folder, skips test files that do not import enzyme, and counts the Enzyme calls in each remaining file:

// scripts/enzyme-inventory.mjs
// Count the Enzyme APIs each test file uses, to size the migration.
// Usage: node scripts/enzyme-inventory.mjs src
import { readFileSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';

const apis = {
  shallow: /\bshallow\(/g,
  mount: /\bmount\(/g,
  'find()': /\.find\(/g,
  'simulate()': /\.simulate\(/g,
  'state()': /\.state\(/g,
  'setState()': /\.setState\(/g,
  'setProps()': /\.setProps\(/g,
  'instance()': /\.instance\(/g,
  snapshot: /toMatchSnapshot\(/g,
};

function testFiles(dir) {
  return readdirSync(dir).flatMap((name) => {
    const full = path.join(dir, name);
    if (name === 'node_modules') return [];
    if (statSync(full).isDirectory()) return testFiles(full);
    return /\.(test|spec)\.[jt]sx?$/.test(name) ? [full] : [];
  });
}

for (const file of testFiles(process.argv[2] ?? 'src')) {
  const source = readFileSync(file, 'utf8');
  if (!/from ['"]enzyme['"]/.test(source)) continue;
  const used = Object.entries(apis)
    .map(([api, re]) => [api, (source.match(re) ?? []).length])
    .filter(([, count]) => count > 0)
    .map(([api, count]) => api + ' ' + count);
  console.log(file.split(path.sep).join('/') + ': ' + used.join(', '));
}

Run against the sample project, which holds the CartSummary tests and a second file for a CouponForm component, it prints one line per Enzyme test file:

$ node scripts/enzyme-inventory.mjs src
src/CartSummary.enzyme.test.jsx: shallow 2, mount 2, find() 4, simulate() 2, state() 1, setProps() 1
src/CouponForm.enzyme.test.jsx: mount 2, state() 1, setState() 1, instance() 1, snapshot 1

The counts are text matches, not a parse, so find() also counts Array.prototype.find. Use them to order the work: files limited to find(), simulate() and text() convert mechanically, every state(), setState() and instance() call needs a new assertion, and every snapshot needs a decision.

Step 2: Install React Testing Library Next to Enzyme

Install the Testing Library versions that match the React major you run today, not the one you plan to reach. The npm registry lists a peer range of ^18.0.0 || ^19.0.0 for @testing-library/react 16.3.3, and a dry run in a React 17.0.2 project stopped with ERESOLVE; 12.1.5 is the last release that accepts React below 18.

# React 18 or 19
npm install -D @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-dom
# React 16 or 17
npm install -D @testing-library/react@12 @testing-library/dom@8 @testing-library/user-event@14 @testing-library/jest-dom@6.9.1
  • React 18 or 19 - the first command resolved @testing-library/react 16.3.3, @testing-library/dom 10.4.2, @testing-library/user-event 14.6.9 and @testing-library/jest-dom 7.0.1 on React 19.3.1. Since version 16, React Testing Library declares @testing-library/dom as a peer dependency, so the command names it.
  • React 16 or 17 - the second command resolved React Testing Library 12.1.5 with a single copy of @testing-library/dom 8.20.1 on React 17.0.2. Without the @testing-library/dom@8 pin, npm added 10.4.2 for user-event next to the 8.20.1 that React Testing Library 12 depends on.
  • jest-dom and Node.js - the jest-dom release notes for 7.0.0 make @testing-library/dom a required peer dependency and Node.js 22 the minimum. 6.10.0 shipped the same peer and Node.js range by mistake, and npm now marks it deprecated with a pointer to 6.9.1, so on React 16 or 17, or on Node.js older than 22, pin 6.9.1.
  • Jest environment - Jest's DOM manipulation docs say the jest-environment-jsdom package must be installed to use the jsdom test environment, so confirm it is in your devDependencies. Suites that run TypeScript tests as native ES modules need more Jest config, which the Jest ESM TypeScript guide walks through.
  • Vitest - the Vitest 5 migration guide requires Node.js 22.12.0 or newer and Vite 6.4.0 or newer; if the move off Enzyme is also a runner change, the Vitest vs Jest comparison covers that decision.

Step 3: Replace shallow() and mount() with render()

Both Enzyme renderers become render(), which Testing Library's migration guide calls "more similar to Enzyme's mount" because it renders the whole tree. The render options cover what Enzyme's mount options did:

  • shallow() and mount() - shallow(<CartSummary items={items} onCheckout={onCheckout} />) and the same mount() call both become render(<CartSummary items={items} onCheckout={onCheckout} />), with no wrapper variable, because queries come from screen.
  • attachTo - mount(<App />, { attachTo: node }) becomes render(<App />, { container: node }).
  • wrappingComponent - mount(<App />, { wrappingComponent: ThemeProvider }) becomes render(<App />, { wrapper: ThemeProvider }), the option React Testing Library's API docs give for rendering providers around the element.
  • Children that cannot run in jsdom - where shallow() was hiding a child that breaks in jsdom, mock that child's module rather than the whole tree.
// RecommendationsCarousel uses IntersectionObserver, which jsdom lacks.
jest.mock('./RecommendationsCarousel', () => ({
  __esModule: true,
  default: () => null,
}));

// Vitest equivalent:
// vi.mock('./RecommendationsCarousel', () => ({ default: () => null }));

Jest's jest.mock documentation requires __esModule: true when a factory replaces a default export, and Vitest's vi.mock documentation requires the default key. Keep these mocks rare, since Testing Library's migration guide advises against mocking components in general.

Step 4: Replace find() with screen Queries

Enzyme's find() takes CSS selectors and component names, while React Testing Library's queries take what a user perceives. The queries documentation ranks them, with getByRole as "your top preference for just about everything" and getByTestId "only recommended for cases where you can't match by role or text".

  • Text - wrapper.find('.total').text() becomes screen.getByText('Total: 25.00').
  • Buttons - wrapper.find('button.checkout') becomes screen.getByRole('button', { name: 'Checkout' }).
  • Form fields - wrapper.find('input[name="coupon"]') becomes screen.getByLabelText('Coupon code'), which only finds the field if its label is attached to it.
  • Components - wrapper.find(CartSummary) has no equivalent; query something the component renders, such as screen.getByRole('region', { name: 'Cart summary' }) for its labeled section.
  • Absence - expect(wrapper.find('.error').exists()).toBe(false) becomes expect(screen.queryByText('Code expired')).not.toBeInTheDocument(), because queryBy returns null instead of throwing.
  • Async content - an element that appears after a promise resolves needs await screen.findByText('Code applied'), which retries until the element appears or the default 1000 ms timeout passes.

Step 5: Replace simulate() with userEvent

Create a user with userEvent.setup() before rendering, as the user-event docs recommend, and await every interaction:

  • Clicks - wrapper.find('.coupon-toggle').simulate('click') becomes await user.click(screen.getByRole('button', { name: 'Add coupon' })).
  • Typing - simulate('change', { target: { value: 'SAVE10' } }) becomes await user.type(screen.getByLabelText('Coupon code'), 'SAVE10'), which types the code one character at a time.
  • Keys - simulate('keydown', { key: 'Escape' }) becomes await user.keyboard('{Escape}'), where the name in braces is a KeyboardEvent key.
  • Form submission - simulate('submit') becomes a click on the form's submit button, so the onSubmit handler receives a real submit event and event.preventDefault() needs no mock.

Step 6: Assert on Rendered Output Instead of State and Props

This step takes judgment, because each assertion on an internal value becomes an assertion on what that value renders:

  • state() - expect(wrapper.state('couponOpen')).toBe(true) becomes a check that the Coupon code field and the Hide coupon button are on screen.
  • setState() - wrapper.setState({ code: 'SAVE10' }) becomes typing the code into the field.
  • setProps() - wrapper.setProps({ items: [] }) becomes rerender(<CartSummary items={[]} onCheckout={onCheckout} />), although the React Testing Library API docs add that "It'd probably be better if you test the component that's doing the prop updating".
  • instance() - wrapper.instance().applyCode() becomes a click on the button that calls applyCode.
  • prop() - wrapper.find('button.checkout').prop('disabled') becomes expect(button).toBeDisabled() from jest-dom.
  • update() and debug() - delete wrapper.update(), which has no equivalent because queries read the live DOM, and replace wrapper.debug() with screen.debug().

Applied to CartSummary, the four Enzyme tests become:

// src/CartSummary.test.jsx (after)
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CartSummary from './CartSummary';

const items = [{ id: 1, price: 12.5, qty: 2 }];

test('shows the total', () => {
  render(<CartSummary items={items} onCheckout={() => {}} />);
  expect(screen.getByText('Total: 25.00')).toBeInTheDocument();
});

test('opens the coupon field', async () => {
  const user = userEvent.setup();
  render(<CartSummary items={items} onCheckout={() => {}} />);

  await user.click(screen.getByRole('button', { name: 'Add coupon' }));

  expect(screen.getByLabelText('Coupon code')).toBeInTheDocument();
  expect(screen.getByRole('button', { name: 'Hide coupon' })).toBeInTheDocument();
});

test('passes the total to onCheckout', async () => {
  const user = userEvent.setup();
  const onCheckout = jest.fn();
  render(<CartSummary items={items} onCheckout={onCheckout} />);

  await user.click(screen.getByRole('button', { name: 'Checkout' }));

  expect(onCheckout).toHaveBeenCalledWith(25);
});

test('disables checkout when the cart empties', () => {
  const { rerender } = render(<CartSummary items={items} onCheckout={() => {}} />);

  rerender(<CartSummary items={[]} onCheckout={() => {}} />);

  expect(screen.getByRole('button', { name: 'Checkout' })).toBeDisabled();
});

The second test now checks what a shopper sees after the click instead of the couponOpen flag, so it keeps passing if CartSummary becomes a function component with a useState hook. The fourth uses rerender for the prop change, the one place the file still sets props directly.

Step 7: Remove the Enzyme Adapter and Block New Imports

When the inventory script prints nothing, remove Enzyme's setup. A typical Jest project registers the adapter in a setup file and the enzyme-to-json serializer in its config:

// src/setupTests.js (before)
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

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

// jest.config.js (before)
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  snapshotSerializers: ['enzyme-to-json/serializer'],
};

After the migration, the setup file only loads the jest-dom matchers and the serializer line is gone. The Jest tutorial covers the rest of the configuration options.

// src/setupTests.js (after)
import '@testing-library/jest-dom';

// jest.config.js (after)
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};

On Vitest, import the /vitest entry point of jest-dom instead, and set globals: true or call cleanup() yourself: React Testing Library's API docs say cleanup runs automatically only when the test framework injects a global afterEach().

// vitest.config.js
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/setupTests.js'],
  },
});

// src/setupTests.js
import '@testing-library/jest-dom/vitest';

Then uninstall the Enzyme packages:

npm uninstall enzyme enzyme-adapter-react-16 enzyme-to-json @types/enzyme

Finally, stop Enzyme from returning in a new test file. ESLint's core no-restricted-imports rule reports any import you list, with your own message:

// eslint.config.mjs
export default [
  {
    files: ['**/*.test.{js,jsx}'],
    languageOptions: {
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
    rules: {
      'no-restricted-imports': ['error', {
        paths: [{
          name: 'enzyme',
          message: 'Write new tests with @testing-library/react.',
        }],
        patterns: [{
          group: ['enzyme-*', '@*/enzyme-adapter-*'],
          message: 'Enzyme adapters and serializers are being removed.',
        }],
      }],
    },
  },
];

Run on the sample project while both test files still imported Enzyme, ESLint 9.39.4 reported each import:

$ npx eslint src

src/CartSummary.enzyme.test.jsx
  1:1  error  'enzyme' import is restricted from being used. Write new tests with @testing-library/react  no-restricted-imports

src/CouponForm.enzyme.test.jsx
  1:1  error  'enzyme' import is restricted from being used. Write new tests with @testing-library/react                             no-restricted-imports
  2:1  error  'enzyme-to-json' import is restricted from being used by a pattern. Enzyme adapters and serializers are being removed  no-restricted-imports

✖ 3 problems (3 errors, 0 warnings)

Keep the rule after the last file is converted, so the pull request that reintroduces Enzyme fails lint before review.

What Happens to Enzyme Snapshot Tests?

Enzyme snapshot tests usually pass the wrapper through enzyme-to-json, which serializes the React tree with its props. Once Step 7 removes the serializer, those snapshots stop matching, and the inventory script's snapshot count tells you how many decisions are waiting. The snapshot testing guide covers why large snapshots break so often; for each Enzyme snapshot, choose one of these:

  • Delete it - a snapshot of a whole tree that reviewers approve without reading checks very little; replace it with the explicit assertions from Step 6.
  • Re-record it as DOM - React Testing Library's API docs describe asFragment() as returning "a DocumentFragment of your rendered component", so the new snapshot records rendered HTML instead of component props.
  • Move it to screenshots - if the snapshot existed to catch visual changes, jsdom never painted the component, so a serialized tree could not show the change.
test('matches the cart summary markup', () => {
  const { asFragment } = render(<CartSummary items={items} onCheckout={() => {}} />);
  expect(asFragment()).toMatchSnapshot();
});

For the visual case, TestMu AI's SmartUI visual testing takes over at the component level. Its Storybook integration captures every story in a Storybook instance across the browsers and viewports in its config, compares each capture with the approved baseline, and lets a reviewer approve or reject every change. The SmartUI Storybook docs give the commands for a static Storybook build:

npm install @lambdatest/smartui-storybook -g
export PROJECT_TOKEN="<your SmartUI project token>"
npm run build-storybook
smartui storybook ./storybook-static --config .smartui.json

Write the CartSummary states you used to snapshot, such as an empty cart and an open coupon field, as stories, and each one gets a baseline of its own.

Note

Note: Keep the behavior checks in React Testing Library and let SmartUI from TestMu AI catch the visual changes jsdom cannot see. Start free with TestMu AI

React Testing Library vs Enzyme: Which Should You Use in 2026?

Use React Testing Library for every new test. Enzyme is worth keeping only as a bridge while an existing suite migrates, and the right bridge depends on the React version the suite runs on today:

Your situationWhat to do
Starting a new React test suiteUse React Testing Library 16.3.3 with user-event and jest-dom, on Jest 30 or Vitest 5; Enzyme is not a candidate.
Enzyme suite on React 16Keep enzyme-adapter-react-16 while you migrate, add React Testing Library 12.1.5, and finish before you upgrade React.
Enzyme suite on React 17The community adapter's repository is archived, so use React Testing Library 12.1.5 and migrate before the upgrade.
Enzyme suite on React 18Treat @cfaester/enzyme-adapter-react-18 as temporary, add React Testing Library 16.3.3, and migrate before React 19.
Enzyme tests already on React 19Only one unofficial fork exists, so convert the failing files first and remove Enzyme.
Tests that mostly read state() or call instance()Budget the most time here, because each one needs a new assertion on rendered output.

For the runners, end-to-end tools and component libraries around this choice, see the roundup of React testing libraries, and for a Vitest setup that starts from an empty project, the React unit testing tutorial.

Conclusion

For new tests the React Testing Library vs Enzyme choice is made, so the work is the existing suite. Run the inventory script on your test folder today and convert the file with the fewest state(), setState() and instance() calls first, so the team learns the query and user-event patterns on mechanical changes. Pin the Testing Library versions to the React major you are on, and add the ESLint rule before the second file, so no new Enzyme test lands while the rest migrates.

Once the suite runs on React Testing Library alone, add the coverage jsdom cannot give you: Storybook visual testing on TestMu AI compares each component story with its approved baseline in real browsers.

To run your first test, start free, or
for a full walkthrough of the product.

Author

...

Himanshu Sheth

Blogs: 153

  • Twitter
  • Linkedin

Himanshu Sheth is the Director of Marketing (Technical Content) at TestMu AI, with over 8 years of hands-on experience in Selenium, Cypress, and other test automation frameworks. He has authored more than 130 technical blogs for TestMu AI, covering software testing, automation strategy, and CI/CD. At TestMu AI, he leads the technical content efforts across blogs, YouTube, and social media, while closely collaborating with contributors to enhance content quality and product feedback loops. He has done his graduation with a B.E. in Computer Engineering from Mumbai University. Before TestMu AI, Himanshu led engineering teams in embedded software domains at companies like Samsung Research, Motorola, and NXP Semiconductors. He is a core member of DZone and has been a speaker at several unconferences focused on technical writing and software quality.

Reviewer

...

Srinivasan Sekar

Reviewer

  • Linkedin

Srinivasan Sekar is Director of Engineering at TestMu AI, where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.

Add to Google preferred sources

Summarise with AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

React Testing Library vs Enzyme FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests