mediumEngineering Practices#74

Testing React Native components

Prompt

Write a test for a Counter component using React Native Testing Library. Test that pressing the + button increments the displayed count.

Solution

it('increments count on press', () => {
  const { getByTestId } = render(<Counter />)
  fireEvent.press(getByTestId('inc'))
  expect(getByTestId('count')).toHaveTextContent('1')
})
Mentor's take

React Native Testing Library's design principle is the whole answer: test through the UI contract — what the user sees and does — never through implementation details. This test renders the component, simulates the press a user would make, and asserts on the rendered text. It never touches useState, never reads component internals. That's why it survives refactors: swap useState for useReducer or a store and the test stays green, because the behavior it specifies didn't change. A test that asserts on internal state does the opposite — it breaks on refactors and misses real regressions in what renders.

This is the middle layer of the testing pyramid: many Jest unit tests for pure logic below it, few Detox/Maestro E2E flows above it, and RNTL integration tests carrying the "does the component actually behave" load at a fraction of E2E cost.

Query choice is a signal too: RNTL's guidance prefers user-facing queries — getByText, getByRole, accessibility labels — over testID, because those queries fail when the accessible experience breaks. testID is the pragmatic fallback for elements with no user-visible handle.

Red flag: asserting on state values or snapshot-testing the whole tree instead of asserting the visible outcome — both couple the test to internals.

Say it: "RNTL tests assert through the UI contract — press the button, expect the rendered text — so refactoring internals never breaks them and real regressions always do."