AniUI Academy

Testing React Native Apps

Jest for pure logic, React Native Testing Library for component behavior, and Detox for real end-to-end flows: three layers, three different costs.

9 min read

React Native testing isn't one tool, and treating it like one usually means either testing too little (nothing but a few pure functions) or paying too much (spinning up a simulator to check a date formatter). It's better understood as three layers, each with a real, named tool, and each with a meaningfully different cost.

Layer one: pure logic, with Jest

Jest is React Native's default test runner, and for testing pure logic — code with no dependency on rendering or the native platform at all — it behaves exactly like it would in any plain JavaScript or TypeScript project:

// pricing.js
export function applyDiscount(price, percentOff) {
  if (percentOff < 0 || percentOff > 100) {
    throw new Error("percentOff must be between 0 and 100");
  }
  return price - price * (percentOff / 100);
}
// pricing.test.js
import { applyDiscount } from "./pricing";
 
test("applies a percentage discount correctly", () => {
  expect(applyDiscount(100, 25)).toBe(75);
});
 
test("rejects an out-of-range percentage", () => {
  expect(() => applyDiscount(100, 150)).toThrow();
});

This is exactly the "extract the logic into a plain function" pattern from earlier lessons paying off directly in testing: applyDiscount doesn't know or care that it's used inside a React Native screen. It has no rendering to simulate, no native module to mock, no device to run on — just inputs and an output, which makes it the cheapest, fastest, and most thoroughly testable code in the entire app. Whenever logic can be pulled out of a component and into a plain function like this, it should be — not just for readability, but because it moves that logic into the cheapest layer of the testing pyramid.

Layer two: component behavior, with React Native Testing Library

Once logic is tangled up with actual rendering — a form that shows a validation message, a button that's disabled until certain fields are filled — pure Jest isn't enough on its own, and this is where React Native Testing Library comes in. It deliberately mirrors React Testing Library's philosophy from the web track: render the component, find things the way a user would (visible text, accessible role), interact with it, and assert on what actually shows up — not on a component's internal state.

import { render, screen, fireEvent } from "@testing-library/react-native";
import SignupForm from "./SignupForm";
 
test("shows a validation message for an empty email", () => {
  render(<SignupForm />);
 
  fireEvent.press(screen.getByRole("button", { name: /sign up/i }));
 
  expect(screen.getByText(/email is required/i)).toBeTruthy();
});

Notice this test never reaches into SignupForm's internals — it presses the button the way a user would tap it, and checks for the message a user would actually see. Exactly like the web version of this philosophy, this keeps the test resilient to internal refactors (switching from useState to useReducer inside the form, say) as long as the user-visible behavior doesn't change.

Layer three: real end-to-end flows, with Detox

Neither Jest nor React Native Testing Library actually runs your app on a device or simulator — they run components in a JS test environment that approximates rendering, which is fast, but doesn't prove the real, compiled app actually works when a real user opens it. Detox is a real, named end-to-end testing tool built specifically for React Native: it launches the actual app on a real or simulated device and drives it through genuine user flows — tapping real buttons, navigating real screens, waiting for real native transitions to finish — the same way a human tester would, just scripted.

// A conceptual Detox flow — exact API syntax varies by version
describe("Login flow", () => {
  it("logs in and lands on the home screen", async () => {
    await element(by.id("email-input")).typeText("user@example.com");
    await element(by.id("password-input")).typeText("correct-password");
    await element(by.id("login-button")).tap();
 
    await expect(element(by.text("Welcome back"))).toBeVisible();
  });
});

This layer is the most convincing kind of test — it's testing the actual system a user experiences, native rendering included, not a simulation of it — but it's also the heaviest: slower to run, needing a real build and a real (or simulated) device, and more fragile to environment issues than a plain Jest test ever is.

Why the layers get progressively heavier

The honest shape of this is a pyramid, and it's shaped that way for a real reason, not tradition: pure logic tests are nearly free and should cover as much business logic as possible; component tests cost more (rendering, simulated interaction) and should cover the behavior that actually depends on rendering; end-to-end tests are the most expensive and slowest, and earn their cost specifically for the handful of critical flows (login, checkout, the core action of the app) where "does this actually work on a real device" is worth the weight. Reaching for Detox to test a date formatter would be spending the most expensive tool on the cheapest problem — the layers exist precisely so that doesn't have to happen.

What to remember

  • Jest is React Native's default test runner and works identically to any JS/TS project for pure logic — no rendering, no device, no native environment involved.
  • Extracting logic into plain functions (a pattern from earlier lessons) directly pays off here: it's the cheapest, most thoroughly testable code in the app.
  • React Native Testing Library applies React Testing Library's behavior-first philosophy to React Native components — query like a user, assert on output, not internals.
  • Detox drives the actual compiled app through real user flows on a real or simulated device — the only layer that tests real native rendering, at real cost in speed and setup.
  • The three layers get progressively heavier for a reason: cover as much as possible with the cheapest layer, and reserve end-to-end tests for the flows that truly need real-device proof.

Check yourself

4 questions · pass 3/4 to unlock Shipping to the App Stores

up to 50
  1. 1.What does Jest give you in a React Native project?

  2. 2.What is React Native Testing Library's philosophy, and how does it compare to React Testing Library on the web?

  3. 3.What makes plain, pure-logic functions the cheapest and easiest thing to test thoroughly in a React Native app?

  4. 4.What does Detox provide that Jest and React Native Testing Library don't?

4 left to answer