# 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 }; ```