cypress vs enzyme vs jest-dom vs react-testing-library
Modern React Testing Strategies: E2E, Component, and DOM Assertions
cypressenzymejest-domreact-testing-librarySimilar Packages:

Modern React Testing Strategies: E2E, Component, and DOM Assertions

cypress is an end-to-end (E2E) testing framework that runs in a real browser, allowing developers to test the entire application flow from the user's perspective. enzyme was a popular utility for React component testing that provided shallow rendering and deep inspection capabilities, but it is now deprecated and incompatible with modern React versions. jest-dom is a lightweight library providing custom DOM matchers (like toBeInTheDocument) to make assertions more readable and focused on accessibility. react-testing-library (now part of @testing-library/react) is the industry standard for component testing, encouraging tests that mimic user interactions rather than implementation details.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
cypress050,9374.48 MB1,0884 days agoMIT
enzyme019,823-2827 years agoMIT
jest-dom0---7 years ago-
react-testing-library0---7 years ago-

Modern React Testing Strategies: E2E, Component, and DOM Assertions

Testing a React application requires a layered approach. You need fast unit tests to verify component logic, integration tests to ensure components work together, and end-to-end (E2E) tests to validate the full user journey. The tools cypress, enzyme, jest-dom, and react-testing-library each occupy specific layers in this strategy. However, the landscape has shifted dramatically in recent years, with one major tool becoming obsolete and others rising as the definitive standard.

🚨 Critical Status Check: The End of Enzyme

Before diving into implementation, we must address the elephant in the room. enzyme is deprecated.

For years, enzyme was the go-to solution for React testing because it allowed "shallow rendering." This meant you could test a component in isolation without rendering its children. It provided powerful APIs to dig into internal state and instance methods.

However, enzyme relies on accessing React's internal data structures. With the release of React 17 and especially React 18 (which introduced concurrent rendering), these internals changed fundamentally. The enzyme maintainers have stated they cannot support React 18 without a complete rewrite that contradicts the library's original design philosophy.

Consequence: If you start a new project today, do not install enzyme. It will not work with modern React. Your tests will fail, and you will be stuck on an old version of React.

// ❌ AVOID: Enzyme is deprecated and incompatible with React 18+
import { shallow } from 'enzyme';

// This approach encourages testing implementation details
const wrapper = shallow(<MyComponent />);
wrapper.instance().someInternalMethod(); // Bad practice: coupling to class internals

Instead, the community has unified around react-testing-library, which works seamlessly with React 18 and future versions.

🧪 Component Testing: Implementation Details vs. User Behavior

The core philosophical difference between the old way (enzyme) and the new way (react-testing-library) is what you test.

enzyme encouraged testing implementation details. You would check if a specific class name existed, if a state variable was set, or if a child component was rendered with specific props. This made tests brittle; if you refactored the component's internal structure but the UI looked the same, the test would fail.

react-testing-library operates on a simple principle: Test what the user sees and does. It discourages accessing internal state. Instead, you query the DOM by text content, accessibility roles (like button or heading), or labels. This ensures your tests verify the actual experience.

Rendering and Querying

react-testing-library renders the component into a real DOM node (using jsdom by default) and provides queries that mimic how a user finds elements.

// ✅ PREFERRED: React Testing Library
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { Counter } from './Counter';

test('increments count when button is clicked', async () => {
  render(<Counter />);
  
  // Query by role (accessibility-first)
  const button = screen.getByRole('button', { name: /increment/i });
  
  // Simulate user interaction
  await userEvent.click(button);
  
  // Assert based on visible output
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

In contrast, an enzyme test would have looked like this (which you should no longer write):

// ❌ DEPRECATED: Enzyme approach
import { shallow } from 'enzyme';
import Counter from './Counter';

test('increments count', () => {
  const wrapper = shallow(<Counter />);
  
  // Accessing internal state - brittle!
  expect(wrapper.state('count')).toBe(0);
  
  // Finding by CSS class or component type
  wrapper.find('.increment-btn').simulate('click');
  
  expect(wrapper.state('count')).toBe(1);
});

🔍 Making Assertions Readable with Jest-DOM

While react-testing-library handles rendering and querying, it relies on jest-dom for assertions. Standard Jest matchers like toBe() or toEqual() are often too low-level for DOM testing. You don't care if an element is null; you care if it is visible or disabled.

jest-dom extends Jest's expect with semantic matchers. These matchers check not just existence, but visibility and accessibility states.

Why jest-dom Matters

Without jest-dom, you might write verbose and unclear checks:

// ❌ Without jest-dom: Verbose and unclear intent
const button = container.querySelector('button');
expect(button).not.toBeNull();
expect(button.disabled).toBe(true);
expect(button.style.display).toBe('none');

With jest-dom, the intent is obvious and the code is shorter:

// ✅ With jest-dom: Clear and semantic
import '@testing-library/jest-dom'; // Enables custom matchers

const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(button).not.toBeVisible();

react-testing-library automatically includes jest-dom in its setup in many configurations, but understanding that they are separate packages is key. jest-dom can also be used with cypress or vanilla DOM tests.

🌐 End-to-End Testing with Cypress

While react-testing-library excels at component logic, it runs in a Node.js environment (via jsdom). It cannot test real browser behaviors like network throttling, actual browser rendering quirks, or complex interactions across multiple tabs.

This is where cypress shines. Cypress runs inside a real browser (Chrome, Firefox, Electron, etc.). It tests your application exactly as a user experiences it, from the URL bar to the final pixel.

Key Differences in Approach

  1. Scope: react-testing-library tests isolated components or small trees. cypress tests the full running application.
  2. Speed: react-testing-library tests are milliseconds fast. cypress tests take seconds because they boot a browser.
  3. Syntax: Cypress uses a chained API that automatically retries commands until the element appears, handling asynchronous timing issues gracefully.

Comparing a Login Test

Using react-testing-library (Integration Test): You mock the API call. You verify the component handles the success state.

// ✅ React Testing Library: Fast, isolated, mocks network
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import Login from './Login';

test('shows success message after login', async () => {
  // Mock the API
  global.fetch = jest.fn(() => 
    Promise.resolve({ json: () => Promise.resolve({ token: 'abc' }) })
  );

  render(<Login />);
  
  await userEvent.type(screen.getByLabelText(/username/i), 'user123');
  await userEvent.type(screen.getByLabelText(/password/i), 'pass123');
  await userEvent.click(screen.getByRole('button', { name: /log in/i }));

  // Verify UI update
  expect(await screen.findByText('Welcome back!')).toBeInTheDocument();
});

Using cypress (E2E Test): You hit the real API (or a seeded test database). You verify the URL changes and the full flow works.

// ✅ Cypress: Real browser, real network (or intercepted)
describe('Login Flow', () => {
  it('successfully logs in and redirects', () => {
    // Visit the real URL
    cy.visit('/login');

    // Cypress automatically retries finding these elements
    cy.get('input[name="username"]').type('user123');
    cy.get('input[name="password"]').type('pass123');
    
    cy.get('button').contains('Log In').click();

    // Assert URL change and content
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome back!').should('be.visible');
  });
});

🛠️ Handling Asynchronous UI

Both libraries handle async UI (like loading spinners), but they do it differently.

react-testing-library provides explicit helpers like findBy queries that return a promise and wait for the element to appear.

// React Testing Library: Explicit async/await
// getBy... throws immediately if not found
// findBy... waits up to 1 second (default) for the element
const submitButton = screen.getByRole('button', { name: /submit/i });
await userEvent.click(submitButton);

// Wait for the success message to appear
const successMsg = await screen.findByText('Saved successfully');
expect(successMsg).toBeInTheDocument();

cypress handles waiting implicitly. Almost every command retries until the condition is met or the timeout is reached. You rarely need to write async/await in Cypress tests.

// Cypress: Implicit waiting
cy.get('button').contains('Submit').click();

// Cypress automatically waits for the text to appear
cy.contains('Saved successfully').should('be.visible');

📊 Summary: When to Use Which Tool

| Feature | enzyme | react-testing-library | jest-dom | cypress | | :--- | :--- | :--- | :--- :--- | | Status | ❌ Deprecated | ✅ Industry Standard | ✅ Essential Utility | ✅ E2E Leader | | Environment | Node (jsdom) | Node (jsdom) | Node (jsdom) | Real Browser | | Philosophy | Internal State | User Behavior | Semantic Assertions | User Journey | | Speed | Fast | Fast | N/A (Assertions) | Slow | | React 18 | ❌ No Support | ✅ Full Support | ✅ Compatible | ✅ Compatible | | Best For | Legacy Maintenance | Unit/Integration Tests | Writing Assertions | Critical Flows |

💡 The Winning Strategy

For a modern, robust testing architecture, combine these tools as follows:

  1. Base Layer: Use react-testing-library for 90% of your tests. Write unit and integration tests for your components. Focus on accessibility roles and user interactions.
  2. Assertion Layer: Install jest-dom to make your assertions readable and semantic. It turns vague checks into clear statements of intent.
  3. Safety Net: Use cypress for a small suite of critical end-to-end tests. Cover the "happy paths" (e.g., sign up, purchase, login) to ensure the whole system integrates correctly.
  4. Migration: If you have existing enzyme tests, treat them as technical debt. Do not write new ones. Gradually rewrite them using react-testing-library as you touch those files for feature work.

By shifting away from internal implementation details (enzyme) and toward user-centric testing (react-testing-library + jest-dom) while reserving heavy browser tests (cypress) for high-value scenarios, you create a test suite that is fast, reliable, and resilient to refactoring.

How to Choose: cypress vs enzyme vs jest-dom vs react-testing-library

  • cypress:

    Choose cypress when you need to verify critical user journeys across your entire application, such as login flows, checkout processes, or cross-browser compatibility. It is essential for catching integration bugs that unit tests miss, but it is slower to run and should be reserved for high-value scenarios rather than every component.

  • enzyme:

    Do NOT choose enzyme for new projects. It is officially deprecated, no longer maintained, and fundamentally incompatible with React 18+ due to its reliance on internal React APIs. If you inherit a legacy codebase using it, plan a migration strategy to @testing-library/react immediately.

  • jest-dom:

    Choose jest-dom whenever you are writing tests that interact with the DOM, regardless of whether you use React, Vue, or vanilla JS. It pairs naturally with react-testing-library to replace generic assertions with semantic ones (e.g., toBeInTheDocument), making your test intent clearer and more robust against minor DOM structure changes.

  • react-testing-library:

    Choose react-testing-library as your primary tool for unit and integration testing of React components. It forces you to write tests that behave like real users (querying by text, role, or label) rather than checking internal state or class names, resulting in tests that refactor safely and verify accessibility by default.

README for cypress

Cypress

Fast, easy and reliable testing for anything that runs in a browser.

What is this?

Cypress comes packaged as an npm module, which is all you need to get started testing.

After installing you'll be able to:

  • Open Cypress from the CLI
  • Run Cypress from the CLI
  • require Cypress as a module

Install

Please check our system requirements.

npm install --save-dev cypress

Documentation

Please visit our documentation for a full list of commands and examples.