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.
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.
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-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.
react-test-renderer (DEPRECATED)react-test-renderer is deprecated and no longer maintained. It will be removed in a future version. As of React 19, you will see a console warning when invoking ReactTestRenderer.create().
This library creates a contrived environment and its APIs encourage introspection on React's internals, which may change without notice causing broken tests. It is instead recommended to use browser-based environments such as jsdom and standard DOM APIs for your assertions.
The React team recommends @testing-library/react as a modern alternative that uses standard APIs, avoids internals, and promotes best practices.
The React team recommends @testing-library/react-native as a replacement for react-test-renderer for native integration tests. This React Native testing-library variant follows the same API design as described above and promotes better testing patterns.
This package provides an experimental React renderer that can be used to render React components to pure JavaScript objects, without depending on the DOM or a native mobile environment.
Essentially, this package makes it easy to grab a snapshot of the "DOM tree" rendered by a React DOM or React Native component without using a browser or jsdom.
Documentation: https://reactjs.org/docs/test-renderer.html
Usage:
const ReactTestRenderer = require('react-test-renderer');
const renderer = ReactTestRenderer.create(
<Link page="https://www.facebook.com/">Facebook</Link>
);
console.log(renderer.toJSON());
// { type: 'a',
// props: { href: 'https://www.facebook.com/' },
// children: [ 'Facebook' ] }
You can also use Jest's snapshot testing feature to automatically save a copy of the JSON tree to a file and check in your tests that it hasn't changed: https://jestjs.io/blog/2016/07/27/jest-14.html.