Back to Home

React Testing Library: Ditching Snapshots

The article describes the transition from Enzyme to React Testing Library in the project. Analysis of snapshot and implementation test problems. Checklists for writing and reviewing tests, recommendations for meetups and migration.

RTL instead of Enzyme: React behavior tests
Advertisement 728x90

From Snapshots to Behavior: Migrating to React Testing Library

In typical projects, tests often focus on markup rather than actual functionality. Snapshot tests capture DOM state—but break with any refactoring: changing CSS classes, reordering attributes, or optimizing rendering. Real logic bugs remain undetected.

A Problematic Test Example with Enzyme

describe("UserProfileCards", () => {
    it("should render", () => {
        const wrapper = shallow(
            <Provider store={store}>
                <UserProfileCards items={mockedUserProfiles} />
            </Provider>
        )
        expect(wrapper).toMatchSnapshot()
    })
})

This test offers no real confidence in component behavior. It checks structure—not what the user actually experiences.

Implementation Coupling: False Positives

Tests tied to internal details create a double problem: they fail during refactoring and miss real bugs.

Google AdInline article slot

Consider a CollapseIcon component with an opened state:

import CollapseIcon from "./CollapseIcon"
import { shallow } from "enzyme"

describe("CollapseIcon", () => {
    it("should render", () => {
        const wrapper = shallow(<CollapseIcon opened={false} />)
        expect(wrapper).toMatchSnapshot()
    })

    it("should react to closed", () => {
        const wrapper = shallow(<CollapseIcon opened={false} />)
        expect(wrapper.find(".collapse__icon").hasClass("collapseIcon_closed")).toEqual(true)
    })

    it("should react to opened", () => {
        const wrapper = shallow(<CollapseIcon opened={true} />)
        expect(wrapper.find(".collapse__icon").hasClass("collapseIcon_opened")).toEqual(true)
    })
})

Problems:

  • Snapshots don’t catch animation logic errors.
  • Class checks break when styles are refactored.
  • Tests pass even if animations are broken.

Another case: tight coupling to localization constants:

Google AdInline article slot
it("should render no-answers button", () => {
    const button = getButton(mockCommentsList[0])
    const noAnswersText = `${mockedTranslation.t(`${tNamespace}no-answers`)}
    expect(button.text()).toBe(noAnswersText)
})

Changing the key in tNamespace breaks the test—even if the button text is correct. Solution: move constants to a shared module imported by both component and tests.

Unclear test descriptions make things worse:

  • "should show tooltip" — no conditions.
  • "should render without tooltipLabel" — no expected outcome.

React Testing Library: Testing User Behavior

RTL shifts the paradigm: tests simulate real user actions and ignore internals. No access to state, props, or methods—only the visible UI.

Google AdInline article slot

Benefits of RTL:

  • User-centric focus: clicks, typing, screen reading.
  • Refactoring-safe: independent of class names, IDs, or internal state.
  • API: render(), screen.getByRole(), fireEvent.click(), waitFor().
  • Async support: findByRole() for data loading.
  • Accessibility: getByLabelText() ensures proper labeling.

RTL is built for resilience: tests reflect real usage, not DOM structure.

Checklist for High-Quality Tests

Writing Tests

  • Purpose: clearly define what’s being tested (e.g., "displays tooltip when isTooltipShow=true").
  • User perspective: actions as a real user would do them—getByRole('button'), fireEvent.
  • Isolation: mocks for APIs; no dependencies between tests.
  • Descriptions: full sentences readable without code.
  • Coverage: happy path + edge cases + error states.

Reviewing Tests

  • Is the goal clear?
  • Are implementation details (classes, state) avoided?
  • Is the test isolated?
  • Are descriptions informative?
  • Are comments constructive?

Organizing the Team Migration

Host a migration workshop: gather problematic tests, analyze examples, transition to RTL. Recommendations:

  • Break sessions into 45-minute chunks with hands-on practice.
  • Refactor tests together via pair programming.
  • Prioritize practice over theory.

Legacy migration: write new tests in RTL, gradually rewrite old ones. Airbnb case: 3,500 files rewritten in 6 weeks using an LLM-powered pipeline (75% automated).

Key Takeaways

  • Mindset shift: from markup to user behavior.
  • RTL vs Enzyme: RTL blocks internal access; Enzyme encourages it.
  • Checklists: mandatory for writing and reviewing.
  • Workshops: dialogue beats monologue.
  • Migration: automation speeds up progress, but understanding the philosophy is essential.

— Editorial Team

Advertisement 728x90

Read Next