Ship with confidence — unit-test logic with Jest, test what users see with React Native Testing Library, and drive the real app end-to-end with Maestro.
Why: three layers catch different bugs. Unit tests (Jest) check pure logic fast. Component tests (React Native Testing Library) render a component and assert on what the user would see and do. End-to-end tests (Maestro) drive the real app on a simulator like a user. Most of your tests should be the cheap lower layers.
many ─ unit (Jest) pure functions, hooks fast
│ ─ component (RNTL) render + interact medium
few ─ end-to-end (Maestro) the real app on a device slowWhy: Jest comes preconfigured in Expo projects (the jest-expo preset). Test pure logic — formatting, validation, reducers — with plain expect assertions. These run in milliseconds and form the base of your test suite.
// utils.test.ts
import { isValidEmail } from './utils'
test('accepts a valid email', () => {
expect(isValidEmail('ada@example.com')).toBe(true)
})
test('rejects a missing @', () => {
expect(isValidEmail('nope')).toBe(false)
})Why: React Native Testing Library renders a component and lets you query it the way a user perceives it — by text and accessibility role — then fire events. You assert on visible output, not implementation details, so tests survive refactors.
// Counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react-native'
import Counter from './Counter'
test('increments on tap', () => {
render(<Counter />)
fireEvent.press(screen.getByText('Tap me'))
expect(screen.getByText('You tapped 1 times')).toBeTruthy()
})Why: E2E tests launch the actual app on a simulator and interact with it like a real user — type, tap, assert what appears — catching integration bugs unit tests miss. Maestro describes a flow in plain YAML (no test code), which makes it simple and resilient to small UI changes. They are slow, so write a few for critical flows (login, checkout), not everything. Run a flow with: maestro test login.yaml.
# login.yaml — a Maestro flow
appId: com.example.myapp
---
- launchApp
- tapOn: "Email"
- inputText: "ada@example.com"
- tapOn: "Password"
- inputText: "password123"
- tapOn: "Log in"
- assertVisible: "Welcome back"