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.
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:
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.
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
No comments yet.