Testing Library vs Enzyme in 2026: React Testing Approaches
TL;DR
Testing Library has won. Enzyme is effectively deprecated for React 18. @testing-library/react (~15M weekly downloads) is the standard for React component testing. Enzyme (~3M downloads, mostly legacy projects) has no official React 18 adapter and development has stalled. If you're on React 18+, use Testing Library. The only reason to use Enzyme today is if you're maintaining a legacy codebase that can't migrate.
Key Takeaways
- @testing-library/react: ~15M weekly downloads — Enzyme: ~3M (npm, March 2026)
- Enzyme has no official React 18 adapter — community adapters exist but are unstable
- Testing Library tests user behavior — Enzyme tests implementation details
- Testing Library is framework-agnostic — works for Vue, Angular, Svelte, DOM
- The philosophy difference matters — Testing Library's approach produces better tests
The Core Philosophy Difference
This isn't just API preference — it's a fundamental difference in testing philosophy.
Enzyme's approach: Test what the component does internally — its state, methods, props, child component instances.
Testing Library's approach: Test what the user sees and does — DOM output, user interactions, accessible elements.
// Enzyme — testing internal state (implementation details)
const wrapper = shallow(<Counter />);
expect(wrapper.state('count')).toBe(0);
wrapper.find('button').simulate('click');
expect(wrapper.state('count')).toBe(1);
// Problem: This breaks if you refactor to useReducer or external state
// Testing Library — testing user behavior
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
// This still works if you change state management internally
The Testing Library test is more resilient to refactoring because it doesn't know or care about implementation details.
React 18 Compatibility
// Enzyme with React 18 — problematic
// The official @wojtekmaj/enzyme-adapter-react-18 community adapter exists
// but is not officially maintained by Enzyme team
// enzyme.config.js (unofficial adapter)
import Enzyme from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-18';
Enzyme.configure({ adapter: new Adapter() });
// Many tests still break due to React 18 concurrent mode changes
// act() warnings are common and difficult to suppress
// Testing Library with React 18 — fully supported
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// @testing-library/react v14+ handles React 18 concurrent features
// No configuration needed — it wraps renders in act() automatically
Component Testing Examples
// Enzyme — shallow rendering (tests component in isolation)
import { shallow, mount } from 'enzyme';
describe('UserCard', () => {
it('renders user name', () => {
const wrapper = shallow(<UserCard user={{ name: 'Alice', role: 'Admin' }} />);
expect(wrapper.find('.user-name').text()).toBe('Alice');
expect(wrapper.find('.role-badge').text()).toBe('Admin');
});
it('calls onEdit when edit button clicked', () => {
const onEdit = jest.fn();
const wrapper = mount(<UserCard user={{ name: 'Alice' }} onEdit={onEdit} />);
wrapper.find('[data-testid="edit-button"]').simulate('click');
expect(onEdit).toHaveBeenCalledWith({ name: 'Alice' });
});
});
// Testing Library — user-centric testing
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('UserCard', () => {
it('renders user name and role', () => {
render(<UserCard user={{ name: 'Alice', role: 'Admin' }} />);
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Admin')).toBeInTheDocument();
});
it('calls onEdit when edit button clicked', async () => {
const onEdit = jest.fn();
render(<UserCard user={{ name: 'Alice' }} onEdit={onEdit} />);
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(onEdit).toHaveBeenCalledWith({ name: 'Alice' });
});
});
Querying Elements
// Enzyme — CSS selector and component-based queries
wrapper.find('.submit-button');
wrapper.find('button');
wrapper.find(SubmitButton); // Find by component type
wrapper.find({ 'data-testid': 'submit' });
wrapper.find('input[type="email"]');
// Testing Library — accessibility-first queries
// Priority order (best to least accessible):
screen.getByRole('button', { name: /submit/i }); // BEST — by ARIA role
screen.getByLabelText('Email'); // Form elements
screen.getByPlaceholderText('Enter email...'); // Inputs
screen.getByText('Submit'); // By text content
screen.getByDisplayValue('existing@email.com'); // Current form values
screen.getByAltText('User avatar'); // Images
screen.getByTitle('Close dialog'); // Title attribute
screen.getByTestId('submit-button'); // LAST RESORT — data-testid
// Testing queries also catch accessibility issues
// If getByRole fails, your component might have an accessibility problem
The Testing Library query priority is intentional — it encourages writing accessible components.
Async Testing
// Enzyme — async testing is verbose
it('loads user data', done => {
const wrapper = mount(<UserProfile userId="123" />);
setImmediate(() => {
wrapper.update();
expect(wrapper.find('.user-name').text()).toBe('Alice');
done();
});
});
// Or with async/await + manual wrapper.update()
it('loads user data', async () => {
const wrapper = mount(<UserProfile userId="123" />);
await act(async () => {
await Promise.resolve();
wrapper.update();
});
expect(wrapper.find('.user-name').text()).toBe('Alice');
});
// Testing Library — async testing is ergonomic
it('loads user data', async () => {
render(<UserProfile userId="123" />);
// findBy* queries wait up to 1000ms for element to appear
const name = await screen.findByText('Alice');
expect(name).toBeInTheDocument();
});
// waitFor — wait for condition to be true
it('shows error on failed load', async () => {
server.use(rest.get('/api/user/:id', (req, res, ctx) => res(ctx.status(500))));
render(<UserProfile userId="123" />);
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});
Migration Path
For large Enzyme codebases, incremental migration is feasible:
// Step 1: Install Testing Library alongside Enzyme
// npm install --save-dev @testing-library/react @testing-library/user-event
// Step 2: Start writing new tests with Testing Library
// Old Enzyme tests still run
// Step 3: Migrate test by test during normal maintenance
// When a component changes, migrate its Enzyme test to Testing Library
// Step 4: Remove Enzyme when all tests migrated
// @testing-library/jest-dom provides the extra matchers:
import '@testing-library/jest-dom';
// Enables: toBeInTheDocument, toBeVisible, toHaveValue, etc.
The migration isn't urgent if your Enzyme tests pass, but React 18+ forces the issue for new features.
When to Choose
Choose Testing Library when:
- Starting any new React project (always)
- Using React 18 (Enzyme's adapter is unofficial)
- You value tests that survive refactoring
- Your team cares about accessibility testing
- Using Vue, Angular, or Svelte (Testing Library has adapters for all)
Keep Enzyme when:
- Large legacy codebase on React 16/17 with thousands of Enzyme tests
- Migration cost isn't justified by business value right now
- Using shallow rendering to test component trees (Testing Library doesn't support shallow)
Compare Testing Library and Enzyme package health on PkgPulse.
See the live comparison
View testing library vs. enzyme on PkgPulse →