Testing React Native Apps: Jest Unit Tests, Component Tests, and Detox E2E
Quality mobile apps are built on solid test foundations. Testing separates apps that work from apps that work reliably. React Native developers have powerful testing tools—Jest, React Native Testing Library, and Detox—that enable comprehensive coverage from unit tests to end-to-end scenarios. This guide covers professional testing strategies for production React Native apps.
Why Testing Matters in Mobile
Mobile apps face unique challenges: diverse devices, OS versions, network conditions, and real user scenarios. Testing catches these issues before users do.
Testing pyramid for React Native:
- Unit tests (70%): Individual functions and hooks
- Component tests (20%): UI logic and user interactions
- E2E tests (10%): Critical user flows on real devices
This distribution maximizes coverage while maintaining reasonable test execution time.
Key Takeaways
- Jest handles unit and component testing with excellent React mocking
- React Native Testing Library lets you test components like users interact with them
- Detox enables reliable E2E testing on real and emulated devices
- Mock strategies for Firebase, async operations, and network calls are essential
- Test organization (unit/component/E2E separation) maintains sustainability
- CI/CD integration ensures tests run automatically on every push
Setting Up Jest for React Native
Jest is the standard test runner for React Native. It's pre-configured but requires key settings for mobile environments.
Jest Configuration
Create or update jest.config.js:
module.exports = {
preset: 'react-native',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.js', '**/*.test.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.js',
'src/**/*.jsx',
'!src/**/*.test.js',
'!src/index.js',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
};
Jest Setup File
Create jest.setup.js for global mocks:
jest.mock('@react-native-async-storage/async-storage', () => ({
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
}));
jest.mock('@react-native-community/netinfo', () => ({
fetch: jest.fn().mockResolvedValue({
isConnected: true,
isInternetReachable: true,
}),
addEventListener: jest.fn(),
}));
global.fetch = jest.fn();
Unit Testing with Jest
Unit tests verify individual functions in isolation. They're fast, focused, and ideal for business logic.
Testing Utility Functions
export const calculateDiscount = (price, discountPercent) => {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Invalid discount percentage');
}
return price * (1 - discountPercent / 100);
};
describe('calculateDiscount', () => {
it('calculates correct discount for valid inputs', () => {
expect(calculateDiscount(100, 20)).toBe(80);
expect(calculateDiscount(50, 50)).toBe(25);
});
it('throws error for invalid discount', () => {
expect(() => calculateDiscount(100, 150)).toThrow(
'Invalid discount percentage'
);
expect(() => calculateDiscount(100, -10)).toThrow();
});
it('handles edge cases', () => {
expect(calculateDiscount(0, 50)).toBe(0);
expect(calculateDiscount(100, 0)).toBe(100);
expect(calculateDiscount(100, 100)).toBe(0);
});
});
Testing Custom Hooks
import {useState} from 'react';
export const useCounter = (initialValue = 0) => {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialValue);
return {count, increment, decrement, reset};
};
import {renderHook, act} from '@testing-library/react-native';
describe('useCounter', () => {
it('initializes with default value', () => {
const {result} = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('initializes with custom value', () => {
const {result} = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increments and decrements correctly', () => {
const {result} = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(6);
act(() => {
result.current.decrement();
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
it('resets to initial value', () => {
const {result} = renderHook(() => useCounter(100));
act(() => {
result.current.increment();
result.current.increment();
});
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(100);
});
});
Testing Async Operations
export const fetchUserData = async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
};
describe('fetchUserData', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('fetches and returns user data', async () => {
const mockUser = {id: 1, name: 'John', email: 'john@example.com'};
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
const result = await fetchUserData(1);
expect(result).toEqual(mockUser);
expect(global.fetch).toHaveBeenCalledWith('/api/users/1');
});
it('throws error on failed fetch', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
});
await expect(fetchUserData(1)).rejects.toThrow(
'Failed to fetch user'
);
});
});
Component Testing with React Native Testing Library
Component tests verify UI logic and user interactions. They test components as users see them.
Basic Component Testing
import {render, screen} from '@testing-library/react-native';
import {Button, Text} from 'react-native';
const WelcomeButton = ({onPress, title}) => (
<Button title={title} onPress={onPress} />
);
describe('WelcomeButton', () => {
it('renders button with correct title', () => {
render(<WelcomeButton title="Press Me" onPress={jest.fn()} />);
expect(screen.getByRole('button', {name: 'Press Me'})).toBeTruthy();
});
it('calls onPress when button is pressed', () => {
const mockPress = jest.fn();
render(<WelcomeButton title="Click" onPress={mockPress} />);
fireEvent.press(screen.getByRole('button'));
expect(mockPress).toHaveBeenCalled();
});
});
Testing with User Interactions
import {render, screen, fireEvent} from '@testing-library/react-native';
import {TextInput, View, Text} from 'react-native';
const LoginForm = ({onSubmit}) => {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const handleSubmit = () => {
onSubmit({email, password});
};
return (
<View>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
testID="email-input"
/>
<TextInput
placeholder="Password"
secureTextEntry
value={password}
onChangeText={setPassword}
testID="password-input"
/>
<Button title="Login" onPress={handleSubmit} testID="login-button" />
</View>
);
};
describe('LoginForm', () => {
it('captures email and password input', () => {
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
fireEvent.changeText(screen.getByTestID('email-input'), 'test@example.com');
fireEvent.changeText(screen.getByTestID('password-input'), 'password123');
fireEvent.press(screen.getByTestID('login-button'));
expect(mockSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
it('validates form before submission', () => {
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
fireEvent.press(screen.getByTestID('login-button'));
expect(mockSubmit).toHaveBeenCalledWith({
email: '',
password: '',
});
});
});
Mocking Firebase
Firebase is common in React Native apps. Proper mocking enables testing without network calls:
jest.mock('@react-native-firebase/auth', () => ({
auth: jest.fn(() => ({
signInWithEmailAndPassword: jest.fn(),
createUserWithEmailAndPassword: jest.fn(),
signOut: jest.fn(),
currentUser: null,
onAuthStateChanged: jest.fn(),
})),
}));
jest.mock('@react-native-firebase/database', () => ({
database: jest.fn(() => ({
ref: jest.fn(() => ({
set: jest.fn(),
once: jest.fn(),
on: jest.fn(),
})),
})),
}));
describe('Auth with Firebase', () => {
it('signs in user with email and password', async () => {
const {signInWithEmailAndPassword} = require('@react-native-firebase/auth').auth();
signInWithEmailAndPassword.mockResolvedValueOnce({
user: {uid: '123', email: 'test@example.com'},
});
const result = await signInWithEmailAndPassword('test@example.com', 'password');
expect(result.user.uid).toBe('123');
});
});
End-to-End Testing with Detox
E2E tests run on real (or emulated) devices, testing complete user flows. Detox is the industry standard for React Native E2E testing.
Setting Up Detox
Install and configure Detox:
npm install detox-cli --global
npm install detox detox-cli --save-dev
detox init -r ios
Writing E2E Tests
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should sign in user with valid credentials', async () => {
await waitFor(element(by.id('email-input')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).multiTap();
await waitFor(element(by.text('Welcome')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.text('Welcome'))).toBeVisible();
});
it('should show error for invalid credentials', async () => {
await element(by.id('email-input')).typeText('invalid@example.com');
await element(by.id('password-input')).typeText('wrongpassword');
await element(by.id('login-button')).multiTap();
await waitFor(element(by.text('Invalid credentials')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.text('Invalid credentials'))).toBeVisible();
});
it('should navigate to details screen', async () => {
await element(by.id('user-list-item')).atIndex(0).multiTap();
await waitFor(element(by.text('User Details')))
.toBeVisible()
.withTimeout(5000);
});
});
Detox Best Practices
describe('Complex User Flow', () => {
it('navigates through multiple screens', async () => {
await device.launchApp();
await element(by.id('home-tab')).multiTap();
await waitFor(element(by.text('Home')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('search-tab')).multiTap();
await waitFor(element(by.text('Search Results')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('search-input')).typeText('React Native');
await element(by.id('search-button')).multiTap();
await waitFor(element(by.text('Results for: React Native')))
.toBeVisible()
.withTimeout(5000);
});
});
Test Organization and Structure
Professional test suites are organized logically:
__tests__/
├── unit/
│ ├── utils/
│ │ ├── calculateDiscount.test.js
│ │ └── formatDate.test.js
│ ├── hooks/
│ │ └── useCounter.test.js
│ └── services/
│ └── api.test.js
├── components/
│ ├── LoginForm.test.js
│ ├── UserList.test.js
│ └── ProfileCard.test.js
├── integration/
│ ├── AuthFlow.test.js
│ └── UserProfile.test.js
└── e2e/
├── LoginFlow.e2e.js
├── Navigation.e2e.js
└── DataSync.e2e.js
CI/CD Integration
Automated testing in CI/CD pipelines catches issues before production.
GitHub Actions Example
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit and component tests
run: npm run test
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
files: ./coverage/lcov.info
- name: Build app
run: npm run build
Running Tests Locally
npm run test # Run all tests
npm run test -- --coverage # With coverage report
npm run test -- --watch # Watch mode
npm run test:e2e # E2E tests only
Testing Best Practices
Unit Tests:
- One assertion per test when possible
- Test success cases and error scenarios
- Mock external dependencies
- Keep tests focused and fast
Component Tests:
- Test user interactions, not implementation
- Use accessible queries (by.text, by.id)
- Test edge cases and error states
- Avoid testing framework details
E2E Tests:
- Test critical user flows only
- Verify end results, not intermediate states
- Use meaningful waits and timeouts
- Test on multiple device configurations
General:
- Aim for >70% code coverage
- Write tests alongside code, not after
- Keep tests maintainable and readable
- Run tests frequently during development
- Integrate tests into CI/CD pipeline
Common Pitfalls to Avoid
Flaky Tests:
- Use explicit waits instead of arbitrary delays
- Avoid testing implementation details
- Mock network requests consistently
Slow Test Suites:
- Keep unit tests fast (under 1ms per test)
- Parallelize E2E tests
- Cache dependencies
Poor Coverage:
- Test behaviors, not lines of code
- Focus on critical paths first
- Avoid 100% coverage for diminishing returns
Testing is an investment that compounds. Initial effort in establishing testing infrastructure and habits pays dividends through fewer production bugs, faster refactoring, and confidence in deployments. By combining Jest unit tests, React Native Testing Library for components, and Detox for E2E flows, you build apps that work reliably across devices, OS versions, and real-world scenarios.
Let's bring your app idea to life
I specialize in mobile and backend development.
Share this article
Related Articles
My Battle With Jest Mocks and Firebase Auth
Mocking @react-native-firebase/auth in Jest should be simple — until it isn’t. Here’s the real-world story of how I struggled with a mysterious jest.fn() bug, what I learned about shared references, and the best practices that finally fixed my test suite.
Advanced Form Handling in React Native: Validation, Multi-Step Forms, and State Management
Master form handling in React Native. Learn validation strategies, async field validation, multi-step wizards, error handling, accessibility, and implement production-ready forms with React Hook Form and custom validators.
API Integration Patterns in React Native: RESTful Services, Caching, and Error Handling
Master API integration in React Native. Learn to build robust API services with error handling, retry logic, request caching, pagination, authentication, rate limiting, and type-safe API clients for production apps.