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.
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.
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();
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
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 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
You need to verify that clicking a button calls a prop function.
react-testing-library// react-testing-library
const handleClick = jest.fn();
render(<Button onClick={handleClick} />);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
You are maintaining a large codebase of class components written in 2018.
enzymestate() 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);
You want to ensure a presentational component's structure hasn't changed accidentally.
react-test-renderer// react-test-renderer
const component = renderer.create(<Avatar src="..." />);
expect(component.toJSON()).toMatchSnapshot();
| Feature | enzyme | react-test-renderer | react-testing-library |
|---|---|---|---|
| Maintenance | โ Unmaintained / Legacy | โ Maintained by React Team | โ Actively Maintained |
| Rendering | Shallow / Mount (Virtual) | Deep (JSON Object) | Deep (Real DOM via jsdom) |
| Querying | CSS Selectors / Components | Tree Traversal | Accessibility Roles / Text |
| Hooks Support | โ Poor / Workarounds | โ Native | โ Native + Utilities |
| Philosophy | Test Implementation | Test Structure | Test Behavior |
| Best For | Legacy Codebases | Snapshot Testing | Modern App Testing |
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.
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.
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.
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.
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.
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.
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 Package | React 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.4 | 15.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() });
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 Package | For Library | Status |
|---|---|---|
enzyme-adapter-preact-pure | preact | (stable) |
enzyme-adapter-inferno | inferno | (work in progress) |
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:
chai-enzyme with Mocha/Chai.jasmine-enzyme with Jasmine.jest-enzyme with Jest.should-enzyme for should.js.expect-enzyme for expect.Using Enzyme with React Native
Using Enzyme with Tape and AVA
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
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
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
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() wrapIf 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(/* ... */);
See the Contributors Guide
Organizations and projects using enzyme can list themselves here.