# react-native-testing
> Best practices for React Native Testing Library (RNTL). Triggers when writing, reviewing, or fixing React Native component tests using RNTL queries, userEvent, async utilities, act() warnings, or Jest matchers. Also activates for test failures involving findBy, waitFor, queryBy, or accessibility queries.
- Author: Paweł Pęcikiewicz
- Repository: wyrwigruz/react-native-skills
- Version: 20260127143903
- Stars: 0
- Forks: 0
- Last Updated: 2026-02-06
- Source: https://github.com/wyrwigruz/react-native-skills
- Web: https://mule.run/skillshub/@@wyrwigruz/react-native-skills~react-native-testing:20260127143903
---
---
name: react-native-testing
description: Best practices for React Native Testing Library (RNTL). Triggers when writing, reviewing, or fixing React Native component tests using RNTL queries, userEvent, async utilities, act() warnings, or Jest matchers. Also activates for test failures involving findBy, waitFor, queryBy, or accessibility queries.
---
# React Native Testing Library (RNTL)
## Core Principles
- Test user-observable behavior, not implementation details
- Prefer accessibility-aligned queries — never add incorrect a11y props just for tests
- Avoid `UNSAFE_*` queries entirely
## Query Priority
Use queries in this order:
1. **`*ByRole`** (preferred) — with `name` option for specificity
2. **`*ByPlaceholderText`** / **`*ByDisplayValue`** — for text inputs
3. **`*ByLabelText`** / **`*ByText`** / **`*ByHintText`** — accessible content
4. **`*ByTestId`** — last resort only
```tsx
// ✅ Good
screen.getByRole("button", { name: "Submit" });
screen.getByRole("heading", { name: /welcome/i });
// ❌ Avoid
screen.getByTestId("submit-button");
```
## Query Variants
| Variant | Use Case |
| ---------- | ----------------------------- |
| `getBy*` | Element exists now (sync) |
| `queryBy*` | Assert element does NOT exist |
| `findBy*` | Wait for element (async) |
**IMPORTANT:** Use `queryBy*` ONLY for non-existence assertions. Use `findBy*` instead of `waitFor(() => getBy*)`.
## Async Patterns
```tsx
// ✅ Waiting for appearance
await screen.findByText("Welcome");
// ✅ Waiting for disappearance
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
// ✅ Waiting for state change
await waitFor(() => {
expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();
});
```
**`waitFor` rules:**
- Never pass empty callback
- Single assertion per callback
- No user interactions inside callback
## Interactions
Always use `userEvent` over `fireEvent`:
```tsx
import { screen, userEvent } from "@pz-mobile-ui/shared/utils/test-utils";
const user = userEvent.setup();
await user.press(screen.getByRole("button", { name: "Submit" }));
await user.type(screen.getByLabelText("Email"), "test@example.com");
await user.clear(screen.getByLabelText("Email"));
```
## act() Usage
**NEVER** wrap these in `act()`:
- `render()`
- `userEvent.*` methods
- `findBy*` queries
- `waitFor*` utilities
**ONLY** use `act()` for:
- Fake timers: `act(() => jest.advanceTimersByTime(1000))`
- Direct hook calls via `renderHook`
- Imperative ref methods
## Test Structure (AAA Pattern)
```tsx
import {
screen,
userEvent,
waitForElementToBeRemoved,
render,
} from "@pz-mobile-ui/shared/utils/test-utils";
describe("ComponentName", () => {
it("submits and shows success", async () => {
// Arrange
const user = userEvent.setup();
render();
// Act
await user.press(screen.getByRole("button", { name: "Submit" }));
// Assert
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
expect(screen.getByText("Success")).toBeOnTheScreen();
});
it("does not render item when disabled", () => {
render();
expect(screen.queryByText("Item")).not.toBeOnTheScreen();
});
});
```
## Common Assertions
```tsx
expect(element).toBeOnTheScreen();
expect(element).toBeEnabled();
expect(element).toBeDisabled();
expect(element).toHaveTextContent("text");
expect(element).toHaveAccessibilityState({ selected: true });
```
## Rules (Enforced)
1. Import from `@pz-mobile-ui/shared/utils/test-utils`, never directly from RNTL
2. Use `screen.*` for all queries — never destructure from `render()`
3. Always `await` `user.*` methods and `findBy*` queries
4. Never call `cleanup()` manually — RNTL auto-cleans
5. Never render in `beforeAll` or module scope
6. Wait for settled UI state before test ends
## Mocking Patterns
```tsx
// Partial mock
jest.mock("@react-navigation/native", () => ({
...jest.requireActual("@react-navigation/native"),
useNavigation: () => ({ navigate: jest.fn() }),
}));
// RN internals
jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
// Spy
jest.spyOn(module, "functionName").mockReturnValue(value);
```
## Custom Render Setup
Create `test-utils.tsx` for shared providers:
```tsx
import { render, RenderOptions } from "@testing-library/react-native";
import { ReactElement } from "react";
type CustomRenderOptions = RenderOptions & {
initialState?: object;
};
function customRender(ui: ReactElement, options: CustomRenderOptions = {}) {
const { initialState, ...renderOptions } = options;
return render(
{ui},
renderOptions,
);
}
export * from "@testing-library/react-native";
export { customRender as render };
```