React Forum
Blog & Guide Responses
Jan 03, 2023 · 04:00 AM

React Testing Library Intro - discussion (2023-01-03)

Started by Keira Santos
externaltesting-librarytestingqueriesaccessibilitycontracts

The React Testing Library intro is short, but it sets a clear posture: test the app the way a user experiences it, and prefer queries that reflect accessibility and intent.

Read the article

What conventions do you use to keep tests stable without coupling to implementation details? Do you treat test selectors as part of the UI contract (and document them), or keep them implicit? How do you balance fast unit tests with route-level integration tests when behavior depends on navigation and data loading?

Comments (10)

Back to latest
Maya Henson
Jan 03, 2023 · 04:06 AM

The best rule: if you're querying by class names, you're probably testing the wrong thing.

Dmitri Kline
Jan 03, 2023 · 04:14 AM

We keep tests stable by making the UI contract visible (names, roles, labels) instead of adding a pile of test-only IDs.

If the UI isn't queryable by intent, that's a product/accessibility issue too.

Related: Testing and Debugging.

Salma Qureshi
Jan 03, 2023 · 04:22 AM

We do have a small set of explicit data attributes, but we version them like a contract surface.

If a selector changes, it should be treated like a breaking change for the test suite.

Priya Liao
Jan 03, 2023 · 04:31 AM

On route-level tests: once your app has real navigation, the boundary isn't a component anymore, it's a route shell.

Integration tests that exercise a flow can actually be *more* stable than brittle unit tests.

Hana Saito
Jan 03, 2023 · 04:44 AM

We basically follow the intro's rule of thumb: reach for roles/labels first, and only use test IDs when the UI contract genuinely has no semantic handle.

ts
render(<Settings />);
await user.click(screen.getByRole('button', { name: /save/i }));
expect(screen.getByRole('status')).toHaveTextContent(/saved/i);
Rowan Pierce
Jan 03, 2023 · 04:56 AM

One thing we did to keep route-level tests stable is render a tiny contract marker in the shell (internal builds only). Then tests can assert on it without coupling to layout:

tsx
return <section data-route="/inbox" data-posture="cache-bypass" />;
Avery Chen
Jan 03, 2023 · 05:08 AM

Counterpoint: a small set of data-testid selectors can be a real contract if you treat them like an API surface.

The problem is when they become a dumping ground for everything that is hard to query.

Keira Santos
Jan 03, 2023 · 05:13 AM

Yeah, that's where I land too. I don't hate test IDs.

I hate when tests depend on them because the UI isn't queryable by intent (which is often an a11y smell).

Avery Chen
Jan 03, 2023 · 05:16 AM

Exactly. If the app has good names/roles, the test suite gets simpler without trying.

Inez Park
Jan 03, 2023 · 05:27 AM

A misunderstanding I see: people think RTL means "never assert anything internal".

For us, asserting on rendered evidence markers (route keys, cache lane) is still testing the UI contract, not implementation details.

Benji Rios
Jan 03, 2023 · 05:41 AM

If you want tests that survive refactors, do the same thing the forum docs push: make behavior visible in render.

Then your tests are just reading the contract you already needed for support/debugging.

Camille Duarte
Jan 03, 2023 · 05:55 AM

Also: avoid over-mocking once you have navigation.

A route shell test that hits a real loader + renders a stable contract marker is usually less brittle than a unit test that mocks three layers and asserts on call order.