enzyme vs react-test-renderer vs react-testing-library
React Component Testing Strategies and Tooling
enzymereact-test-rendererreact-testing-librarySimilar Packages:

React Component Testing Strategies and Tooling

enzyme, react-test-renderer, and react-testing-library are tools used for testing React components, but they serve different purposes and follow different philosophies. enzyme was the dominant testing utility for years, offering shallow rendering and deep introspection into component internals, but it is now in maintenance mode and struggles with modern React features. react-test-renderer is a low-level package provided by the React team that renders React components to pure JavaScript objects without a DOM, often used for snapshot testing. react-testing-library (now part of the Testing Library family) is the current community standard, built on top of react-test-renderer and dom-testing-library, focusing on testing component behavior from the user's perspective rather than implementation details.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
enzyme019,864-2816 years agoMIT
react-test-renderer0244,223988 kB1,1812 months agoMIT
react-testing-library0---7 years ago-

React Testing Tools: Enzyme vs React Test Renderer vs React Testing Library

Testing React applications requires tools that can render components and assert their behavior. For years, enzyme was the go-to choice, but the ecosystem has shifted toward react-testing-library. react-test-renderer sits underneath as a low-level primitive. Understanding the differences is critical for maintaining a healthy, future-proof test suite.

๐Ÿ—๏ธ Rendering Approach: Shallow vs Deep vs User-Centric

enzyme popularized "shallow rendering," which renders a component one level deep without rendering its children. This isolates the unit but couples tests to the component's internal structure.

// enzyme: Shallow rendering
import { shallow } from 'enzyme';
const wrapper = shallow(<MyComponent />);
// Children are not rendered, just placeholders
expect(wrapper.find('ChildComponent')).to.have.length(1);

react-test-renderer renders the full component tree into a JSON-like object. It does not use a real DOM, making it fast but limited in simulating browser events.

// react-test-renderer: Full tree to JSON
import renderer from 'react-test-renderer';
const tree = renderer.create(<MyComponent />).toJSON();
// You inspect the JSON structure directly
expect(tree.children[0].type).toBe('div');

react-testing-library renders components into a real DOM (via jsdom) and encourages querying by accessible roles or text, mimicking how a user interacts with the page.

// react-testing-library: Real DOM rendering
import { render, screen } from '@testing-library/react';
render(<MyComponent />);
// Query by what the user sees
expect(screen.getByRole('button')).toBeInTheDocument();

๐Ÿ” Querying Elements: Implementation vs Accessibility

How you find elements in your tests dictates how fragile they are. enzyme and react-test-renderer often lead to testing implementation details, while react-testing-library enforces accessibility-based queries.

enzyme allows chaining and CSS selectors, which breaks easily if you refactor class names or structure.

// enzyme: CSS selector coupling
const wrapper = shallow(<Form />);
wrapper.find('.submit-btn').simulate('click');
// Breaks if class name changes to .btn-primary

react-test-renderer requires traversing the JSON tree, which is verbose and tightly coupled to the component hierarchy.

// react-test-renderer: Tree traversal
const tree = renderer.create(<Form />).toJSON();
const button = tree.children.find(child => child.type === 'button');
// Breaks if a wrapper div is added around the button

react-testing-library uses semantic queries like getByRole or getByText, which remain stable even if the underlying DOM structure changes.

// react-testing-library: Semantic queries
render(<Form />);
const button = screen.getByRole('button', { name: /submit/i });
fireEvent.click(button);
// Stable even if class names or div structure changes

โš›๏ธ Support for Modern React Features

React has evolved significantly with Hooks, Context, and Concurrent Mode. Tooling support varies wildly across these packages.

enzyme has poor support for Hooks. Testing components with useState or useEffect often requires wrapping them in test harnesses or using unofficial adapters that are no longer maintained.

// enzyme: Hook testing is awkward
// Often requires wrapping in a class component or helper
function testHook() {
  let result;
  function Test() { result = useMyHook(); return null; }
  shallow(<Test />);
  return result;
}

react-test-renderer supports Hooks natively since it is maintained by the React team, but it lacks utilities for triggering effects or waiting for async updates easily.

// react-test-renderer: Native hook support but manual
const root = renderer.create(<Counter />);
root.update(<Counter step={2} />);
// Must manually trigger updates and inspect JSON output

react-testing-library is built for modern React. It handles async updates, effects, and hooks seamlessly with utilities like waitFor and act.

// react-testing-library: First-class async support
render(<AsyncData />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => expect(screen.getByText(/data/i)).toBeInTheDocument());

๐Ÿงช Snapshot Testing Capabilities

Snapshot testing saves the rendered output of a component and compares it against future runs to detect unintended changes.

enzyme can generate snapshots, but they often include internal component details that change frequently, leading to "snapshot fatigue" where developers update snapshots without reviewing them.

// enzyme: Snapshot includes internals
const wrapper = shallow(<Component />);
expect(wrapper).toMatchSnapshot();
// Snapshot may break when internal div structure changes

react-test-renderer is the engine behind most React snapshot testing. It produces clean JSON trees ideal for this purpose.

// react-test-renderer: Standard snapshotting
const tree = renderer.create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
// Widely used for structural regression testing

react-testing-library discourages snapshots of entire component trees because they encourage testing implementation details. Instead, it suggests snapshotting accessibility trees or specific HTML if necessary.

// react-testing-library: Discouraged but possible
const { container } = render(<Component />);
expect(container.firstChild).toMatchSnapshot();
// Prefer testing behavior over static structure

๐ŸŒ Real-World Scenarios

Scenario 1: Testing a Button Click

You need to verify that clicking a button calls a prop function.

  • โœ… Best choice: react-testing-library
  • Why? It simulates real browser events and verifies behavior without relying on internal method references.
// react-testing-library
const handleClick = jest.fn();
render(<Button onClick={handleClick} />);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);

Scenario 2: Legacy Class Component Testing

You are maintaining a large codebase of class components written in 2018.

  • โš ๏ธ Contextual choice: enzyme
  • Why? If the tests already exist and rely on state() or instance() methods, rewriting them might not be cost-effective immediately.
// enzyme
const wrapper = shallow(<ClassComponent />);
wrapper.setState({ count: 1 });
expect(wrapper.instance().method()).toBe(1);

Scenario 3: Pure UI Snapshot Regression

You want to ensure a presentational component's structure hasn't changed accidentally.

  • โœ… Best choice: react-test-renderer
  • Why? It provides the raw JSON tree needed for efficient snapshot comparison without DOM overhead.
// react-test-renderer
const component = renderer.create(<Avatar src="..." />);
expect(component.toJSON()).toMatchSnapshot();

๐Ÿ“Š Summary Table

Featureenzymereact-test-rendererreact-testing-library
MaintenanceโŒ Unmaintained / Legacyโœ… Maintained by React Teamโœ… Actively Maintained
RenderingShallow / Mount (Virtual)Deep (JSON Object)Deep (Real DOM via jsdom)
QueryingCSS Selectors / ComponentsTree TraversalAccessibility Roles / Text
Hooks SupportโŒ Poor / Workaroundsโœ… Nativeโœ… Native + Utilities
PhilosophyTest ImplementationTest StructureTest Behavior
Best ForLegacy CodebasesSnapshot TestingModern App Testing

๐Ÿ’ก The Big Picture

enzyme is a legacy tool ๐Ÿ•ฐ๏ธ. While it served the community well during the class component era, its reliance on implementation details and lack of support for modern React features makes it a liability for new development. Use it only when maintaining older projects.

react-test-renderer is a low-level primitive ๐Ÿงฑ. It powers many higher-level tools but is rarely the best choice for writing application tests directly. It shines in snapshot testing scenarios where you need a lightweight JSON representation of your tree.

react-testing-library is the industry standard ๐Ÿ†. It shifts the focus from "how the component is built" to "how the user experiences the component." This results in tests that are more resilient to refactoring and encourage accessibility best practices.

Final Thought: For any new React project, react-testing-library should be your default choice. It aligns with the direction of the React ecosystem and reduces the maintenance burden of your test suite over time.

How to Choose: enzyme vs react-test-renderer vs react-testing-library

  • enzyme:

    Avoid choosing enzyme for new projects. It is no longer actively maintained and does not support React 16.3+ features like hooks or concurrent mode without significant workarounds. Only consider it if you are maintaining a legacy codebase that already relies heavily on its shallow rendering API and migration is not immediately feasible.

  • react-test-renderer:

    Choose react-test-renderer if you need low-level control for specific snapshot testing scenarios or if you are building a custom testing abstraction. It is rarely used directly for application logic testing because it lacks user-centric queries and DOM simulation, making it better suited for library authors or advanced custom setups.

  • react-testing-library:

    Choose react-testing-library for almost all modern React application testing. It encourages best practices by forcing you to query elements as users do (e.g., by role or text) rather than by class names or component structure. It has full support for modern React features, a massive ecosystem, and integrates seamlessly with Jest and other test runners.

README for enzyme

Enzyme

Join the chat at https://gitter.im/airbnb/enzyme

npm Version License Build Status Coverage Status

Enzyme is a JavaScript Testing utility for React that makes it easier to test your React Components' output. You can also manipulate, traverse, and in some ways simulate runtime given the output.

Enzyme's API is meant to be intuitive and flexible by mimicking jQuery's API for DOM manipulation and traversal.

Upgrading from Enzyme 2.x or React < 16

Are you here to check whether or not Enzyme is compatible with React 16? Are you currently using Enzyme 2.x? Great! Check out our migration guide for help moving on to Enzyme v3 where React 16 is supported.

Installation

To get started with enzyme, you can simply install it via npm. You will need to install enzyme along with an Adapter corresponding to the version of react (or other UI Component library) you are using. For instance, if you are using enzyme with React 16, you can run:

npm i --save-dev enzyme enzyme-adapter-react-16

Each adapter may have additional peer dependencies which you will need to install as well. For instance, enzyme-adapter-react-16 has peer dependencies on react and react-dom.

At the moment, Enzyme has adapters that provide compatibility with React 16.x, React 15.x, React 0.14.x and React 0.13.x.

The following adapters are officially provided by enzyme, and have the following compatibility with React:

Enzyme Adapter PackageReact semver compatibility
enzyme-adapter-react-16^16.4.0-0
enzyme-adapter-react-16.3~16.3.0-0
enzyme-adapter-react-16.2~16.2
enzyme-adapter-react-16.1~16.0.0-0 || ~16.1
enzyme-adapter-react-15^15.5.0
enzyme-adapter-react-15.415.0.0-0 - 15.4.x
enzyme-adapter-react-14^0.14.0
enzyme-adapter-react-13^0.13.0

Finally, you need to configure enzyme to use the adapter you want it to use. To do this, you can use the top level configure(...) API.

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

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

3rd Party Adapters

It is possible for the community to create additional (non-official) adapters that will make enzyme work with other libraries. If you have made one and it's not included in the list below, feel free to make a PR to this README and add a link to it! The known 3rd party adapters are:

Adapter PackageFor LibraryStatus
enzyme-adapter-preact-purepreact(stable)
enzyme-adapter-infernoinferno(work in progress)

Running Enzyme Tests

Enzyme is unopinionated regarding which test runner or assertion library you use, and should be compatible with all major test runners and assertion libraries out there. The documentation and examples for enzyme use mocha and chai, but you should be able to extrapolate to your framework of choice.

If you are interested in using enzyme with custom assertions and convenience functions for testing your React components, you can consider using:

Using Enzyme with Mocha

Using Enzyme with Karma

Using Enzyme with Browserify

Using Enzyme with SystemJS

Using Enzyme with Webpack

Using Enzyme with JSDOM

Using Enzyme with React Native

Using Enzyme with Jest

Using Enzyme with Lab

Using Enzyme with Tape and AVA

Basic Usage

Shallow Rendering

import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';

import MyComponent from './MyComponent';
import Foo from './Foo';

describe('<MyComponent />', () => {
  it('renders three <Foo /> components', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper.find(Foo)).to.have.lengthOf(3);
  });

  it('renders an `.icon-star`', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper.find('.icon-star')).to.have.lengthOf(1);
  });

  it('renders children when passed in', () => {
    const wrapper = shallow((
      <MyComponent>
        <div className="unique" />
      </MyComponent>
    ));
    expect(wrapper.contains(<div className="unique" />)).to.equal(true);
  });

  it('simulates click events', () => {
    const onButtonClick = sinon.spy();
    const wrapper = shallow(<Foo onButtonClick={onButtonClick} />);
    wrapper.find('button').simulate('click');
    expect(onButtonClick).to.have.property('callCount', 1);
  });
});

Read the full API Documentation

Full DOM Rendering

import React from 'react';
import sinon from 'sinon';
import { expect } from 'chai';
import { mount } from 'enzyme';

import Foo from './Foo';

describe('<Foo />', () => {
  it('allows us to set props', () => {
    const wrapper = mount(<Foo bar="baz" />);
    expect(wrapper.props().bar).to.equal('baz');
    wrapper.setProps({ bar: 'foo' });
    expect(wrapper.props().bar).to.equal('foo');
  });

  it('simulates click events', () => {
    const onButtonClick = sinon.spy();
    const wrapper = mount((
      <Foo onButtonClick={onButtonClick} />
    ));
    wrapper.find('button').simulate('click');
    expect(onButtonClick).to.have.property('callCount', 1);
  });

  it('calls componentDidMount', () => {
    sinon.spy(Foo.prototype, 'componentDidMount');
    const wrapper = mount(<Foo />);
    expect(Foo.prototype.componentDidMount).to.have.property('callCount', 1);
    Foo.prototype.componentDidMount.restore();
  });
});

Read the full API Documentation

Static Rendered Markup

import React from 'react';
import { expect } from 'chai';
import { render } from 'enzyme';

import Foo from './Foo';

describe('<Foo />', () => {
  it('renders three `.foo-bar`s', () => {
    const wrapper = render(<Foo />);
    expect(wrapper.find('.foo-bar')).to.have.lengthOf(3);
  });

  it('renders the title', () => {
    const wrapper = render(<Foo title="unique" />);
    expect(wrapper.text()).to.contain('unique');
  });
});

Read the full API Documentation

React Hooks support

Enzyme supports react hooks with some limitations in .shallow() due to upstream issues in React's shallow renderer:

  • useEffect() and useLayoutEffect() don't get called in the React shallow renderer. Related issue

  • useCallback() doesn't memoize callback in React shallow renderer. Related issue

ReactTestUtils.act() wrap

If you're using React 16.8+ and .mount(), Enzyme will wrap apis including .simulate(), .setProps(), .setContext(), .invoke() with ReactTestUtils.act() so you don't need to manually wrap it.

A common pattern to trigger handlers with .act() and assert is:

const wrapper = mount(<SomeComponent />);
act(() => wrapper.prop('handler')());
wrapper.update();
expect(/* ... */);

We cannot wrap the result of .prop() (or .props()) with .act() in Enzyme internally since it will break the equality of the returned value. However, you could use .invoke() to simplify the code:

const wrapper = mount(<SomeComponent />);
wrapper.invoke('handler')();
expect(/* ... */);

Future

Enzyme Future

Contributing

See the Contributors Guide

In the wild

Organizations and projects using enzyme can list themselves here.

License

MIT