React Native Testing
Testing catches bugs before users find them. A tested app is a reliable app. React Native supports three types of testing: unit tests for individual functions, component tests for UI rendering, and end-to-end tests for full user flows. This topic covers the tools and patterns that professional teams use.
Three Testing Levels
Level What It Tests Tool
──────────────────────────────────────────────────────────────────
Unit Tests Pure functions, hooks Jest
Component Tests UI rendering, user events React Native Testing Library
End-to-End (E2E) Full app user flows Detox / Maestro
Testing Pyramid:
▲ E2E Tests (few, slow, high confidence)
▲▲▲ Component Tests (moderate count)
▲▲▲▲▲▲▲ Unit Tests (many, fast, isolated)
Jest — Unit Testing
Expo projects include Jest pre-configured. Jest runs your test files and reports which pass or fail. A unit test checks one piece of logic in isolation — no network, no device, no navigation.
Writing Your First Unit Test
// utils/formatPrice.js
export function formatPrice(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount);
}
// utils/__tests__/formatPrice.test.js
import { formatPrice } from '../formatPrice';
describe('formatPrice', () => {
test('formats a whole number in USD', () => {
expect(formatPrice(10)).toBe('$10.00');
});
test('formats a decimal amount', () => {
expect(formatPrice(9.99)).toBe('$9.99');
});
test('formats in EUR', () => {
expect(formatPrice(25, 'EUR')).toBe('€25.00');
});
test('formats zero', () => {
expect(formatPrice(0)).toBe('$0.00');
});
});
Run tests: $ npx jest Output: ✓ formats a whole number in USD (2ms) ✓ formats a decimal amount (1ms) ✓ formats in EUR (1ms) ✓ formats zero (0ms) Test Suites: 1 passed Tests: 4 passed
Jest Matchers
expect(value).toBe(exact) // strict equality (===) expect(value).toEqual(obj) // deep equality (objects/arrays) expect(value).toBeTruthy() // any truthy value expect(value).toBeFalsy() // null, undefined, 0, false, '' expect(value).toBeNull() // exactly null expect(value).toContain(item) // array or string contains item expect(fn).toThrow() // function throws an error expect(fn).toHaveBeenCalled() // mock function was called expect(fn).toHaveBeenCalledWith(a, b) // called with specific args
React Native Testing Library — Component Tests
React Native Testing Library (RNTL) renders components without a real device and lets you query and interact with them.
npx expo install @testing-library/react-native
Testing a Counter Component
// components/__tests__/Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react-native';
import Counter from '../Counter';
describe('Counter', () => {
test('displays initial count of 0', () => {
render(<Counter />);
expect(screen.getByText('0')).toBeTruthy();
});
test('increments count when button is pressed', () => {
render(<Counter />);
const button = screen.getByText('Tap to Increase');
fireEvent.press(button);
expect(screen.getByText('1')).toBeTruthy();
});
test('increments three times correctly', () => {
render(<Counter />);
const button = screen.getByText('Tap to Increase');
fireEvent.press(button);
fireEvent.press(button);
fireEvent.press(button);
expect(screen.getByText('3')).toBeTruthy();
});
});
Testing a Form
// components/__tests__/LoginForm.test.js
import { render, screen, fireEvent } from '@testing-library/react-native';
import LoginForm from '../LoginForm';
test('shows error when form submitted empty', () => {
render(<LoginForm />);
const loginButton = screen.getByText('Login');
fireEvent.press(loginButton);
expect(screen.getByText('Please fill in all fields.')).toBeTruthy();
});
test('accepts typed email input', () => {
render(<LoginForm />);
const emailInput = screen.getByPlaceholderText('Email');
fireEvent.changeText(emailInput, 'test@example.com');
expect(emailInput.props.value).toBe('test@example.com');
});
RNTL Query Methods
screen.getByText('Login') // find by visible text
screen.getByPlaceholderText('Email') // find by placeholder
screen.getByTestId('submit-btn') // find by testID prop
screen.getByRole('button') // find by accessibility role
screen.queryByText('Error') // returns null if not found
screen.findByText('Loaded') // async — waits for element
fireEvent Methods
fireEvent.press(element) // simulate a tap
fireEvent.changeText(input, 'value') // simulate typing
fireEvent.scroll(list, { ... }) // simulate scroll
Testing Async Components
import { render, screen, waitFor } from '@testing-library/react-native';
// Mock the fetch call
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: 1, title: 'First Post' }]),
})
);
test('loads and displays posts', async () => {
render(<PostsScreen />);
// Loading spinner should be visible first
expect(screen.getByTestId('loading-spinner')).toBeTruthy();
// Wait for data to appear
await waitFor(() => {
expect(screen.getByText('First Post')).toBeTruthy();
});
});
Testing Custom Hooks
import { renderHook, act } from '@testing-library/react-native';
import { useCounter } from '../hooks/useCounter';
test('increments the count', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Mocking Modules
Tests run without a real device. Mock external modules like navigation, AsyncStorage, or expo-location to keep tests fast and deterministic.
// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () => ({
getItem: jest.fn(() => Promise.resolve(null)),
setItem: jest.fn(() => Promise.resolve()),
removeItem: jest.fn(() => Promise.resolve()),
}));
// Mock navigation
const mockNavigate = jest.fn();
jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({ navigate: mockNavigate }),
}));
Test Coverage
npx jest --coverage Output: File | % Stmts | % Branch | % Funcs | % Lines ---------------------|---------|----------|---------|-------- formatPrice.js | 100 | 100 | 100 | 100 Counter.js | 95 | 80 | 100 | 95 Aim for: Critical business logic → 90%+ coverage UI components → 70%+ coverage Utility functions → 100% coverage
Summary
Write unit tests for all pure functions and custom hooks using Jest. Use React Native Testing Library to test component rendering and user interactions. Mock network calls, storage, and navigation to keep tests fast and isolated. Run --coverage to find untested code paths. Focus testing effort on critical business logic first, then expand to UI components.
