Mobile component testing with rntl
Skill Goktug/ai-crew/skills/mobile-component-testing-with-rntl
Writes React Native component tests with @testing-library/react-native that slot into the test-driven-development RED→GREEN cycle. Use when implementing or modifying any RN component, screen, or hook.From its SKILL.md
npx -y skills add Goktug/ai-crew --skill mobile-component-testing-with-rntlAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.0 KB, as published. Nobody here has run it
Mobile Component Testing with RNTL
Overview
Test React Native components with @testing-library/react-native: write a failing test first (RED), make it pass with the minimum implementation (GREEN), then refactor. Query the rendered tree the way a user or an accessibility tool would — by visible text, accessible role, rendered output — never by inspecting component internals. RNTL renders components in a JS-only environment, so tests are fast and do not require a simulator or device.
When to Use
- The Build phase of an ai-crew run is implementing or modifying a React Native component, screen, or custom hook.
- The
developersubagent has been dispatched a task whoseSkills to read firstlist includes this file. - A test for a React Native component is missing or broken.
- The
test-driven-developmentskill is in play and the surface under test is a React Native component (not a Node-side function).
When NOT to use:
- Pure JS/TS utility functions with no React surface — use plain Jest, no RNTL.
- Backend or Next.js work — use the appropriate web testing skill.
- End-to-end flows (multi-screen navigation, real device, real network) — RNTL renders in isolation and does not exercise navigation or networking; use a dedicated end-to-end testing approach for those.
Process
This skill is a specialization of test-driven-development. The RED→GREEN cycle is:
- RED — Write a failing RNTL test that describes what the component should render or do, from a user's perspective. Run it. Confirm it fails for the right reason (component doesn't exist yet, or doesn't render the expected text, or doesn't fire the expected callback).
- GREEN — Write the minimum component code needed to make the test pass. Run the test. Confirm it passes.
- REFACTOR — Clean up the component. Run the test again. It should still pass.
Repeat for each acceptance criterion. One assertion per test is the default; group only when you're testing the same render output from multiple angles.
Required imports
import { render, fireEvent, screen } from '@testing-library/react-native'
Use screen queries (screen.getByText, screen.getByRole) rather than the destructured form returned by render() — it keeps test bodies short and makes refactors easier.
Presence assertions
The examples below use .toBeTruthy() as the presence assertion. getByText and friends return a truthy React element when they find a match and throw when they don't, so .toBeTruthy() reads as "the element exists" and works with a vanilla Jest install.
RNTL also ships a richer matcher .toBeOnTheScreen() (and relatives like .toHaveTextContent, .toBeVisible) in @testing-library/react-native/extend-expect. Those require wiring up a Jest setup file:
// jest.setup.ts
import '@testing-library/react-native/extend-expect'
and in jest.config.js:
module.exports = {
preset: 'react-native',
setupFilesAfterEach: ['<rootDir>/jest.setup.ts'],
}
Use .toBeOnTheScreen() only when the project's Jest config is already set up for the extended matchers. Default to .toBeTruthy() for portability.
Pattern 1 — Render and query by accessible text
For a component that displays a label:
import { render, screen } from '@testing-library/react-native'
import { WelcomeBanner } from './WelcomeBanner'
describe('WelcomeBanner', () => {
it('shows the user name when provided', () => {
render(<WelcomeBanner name="Goktug" />)
expect(screen.getByText('Welcome, Goktug')).toBeTruthy()
})
it('falls back to a generic greeting when name is empty', () => {
render(<WelcomeBanner name="" />)
expect(screen.getByText('Welcome')).toBeTruthy()
})
})
Pattern 2 — Fire a press event and assert the callback
For an interactive button:
import { render, fireEvent, screen } from '@testing-library/react-native'
import { PrimaryButton } from './PrimaryButton'
it('calls onPress when the button is pressed', () => {
const onPress = jest.fn()
render(<PrimaryButton label="Continue" onPress={onPress} />)
fireEvent.press(screen.getByRole('button', { name: 'Continue' }))
expect(onPress).toHaveBeenCalledTimes(1)
})
Prefer getByRole with a name over getByText for interactive elements — it doubles as an accessibility check.
Pattern 3 — Wait for async state with findBy*
For a component that loads data:
import { render, screen } from '@testing-library/react-native'
import { ProfileScreen } from './ProfileScreen'
it('renders the profile name once loaded', async () => {
render(<ProfileScreen userId="42" />)
expect(await screen.findByText('Goktug Aral')).toBeTruthy()
})
findBy* returns a promise that retries the query until it succeeds or times out — use it whenever the assertion depends on state that resolves after render. Do NOT wrap synchronous queries in waitFor unless there's a real async dependency.
Query selection priority
Prefer queries in this order (most accessible first):
getByRole— checks the accessibility treegetByLabelText— for form fieldsgetByPlaceholderText— for text inputsgetByText— for visible textgetByTestId— last resort, only when no semantic query works
Avoid getByTestId unless you have to. A test that needs a testID is often a hint that the component is missing accessible markup.
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "I'll write an end-to-end test for this — it's more realistic." | This skill is for component-level behavior. Component-level RNTL tests are faster, more focused, and run without a device. Use end-to-end tooling only for cross-screen flows that component tests cannot cover. |
| "I'll snapshot-test the whole tree to save time." | Snapshots rot quickly and hide intent. Write explicit assertions for visible text and behavior. |
| "I'll skip RNTL for the loading state — too async." | findBy* exists exactly for this. Use it. |
"I'll use getByTestId everywhere — it's simpler." | getByTestId bypasses the accessibility tree. Use it only when nothing semantic works. |
| "I'll mock the entire React Native module." | Almost never necessary. RNTL renders real components in a JSDOM-like environment. Mock only what you must (network, native modules). |
Red Flags
- About to import from
@testing-library/react(web) instead of@testing-library/react-native. - About to write a snapshot test instead of explicit
screen.getByTextassertions. - About to add an end-to-end test file when a component-level RNTL test would catch the same regression.
- About to use
getByTestIdwhengetByRole/getByTextwould work. - About to wrap a synchronous query in
waitForinstead of usingfindBy*. - About to skip the failing-test (RED) step because "it's just a small component."
- About to mock the entire
react-nativemodule. - About to use
.toBeOnTheScreen()or another RNTL-extended matcher without first checking that@testing-library/react-native/extend-expectis imported in a Jest setup file. Default to.toBeTruthy()unless the project is already wired up for the extended matchers.
Verification
Before reporting PASS on a Build task that uses this skill:
- Each new acceptance criterion has at least one RNTL test
- Tests use
@testing-library/react-native(NOT@testing-library/react) - Tests use
screen.getBy*/findBy*queries, not snapshot assertions - No
getByTestIdintroduced unless no semantic query works - No end-to-end test file added — this skill is for component-level RNTL only
-
npm test(or the project's test command) reports green - The RED→GREEN cycle is observable in the developer's commit history (failing test committed first, then implementation)