Unit Testing in React Native

React Native tests should protect the parts of your app that change often: business rules, form behavior, loading states, empty states, error messages, navigation decisions, and critical user actions. A good test suite does not try to prove that React Native itself works. It proves that your app still behaves correctly when you refactor, upgrade dependencies, or prepare a release build.
Quick Answer
Use Jest for fast JavaScript tests and React Native Testing Library for component tests that interact with the UI like a user. Keep tests small, independent, and explicit. Prefer assertions on visible text, accessibility roles, and user actions instead of internal state or implementation details.
For current Instamobile apps, run test gates from the app folder:
corepack enable
corepack yarn install --immutable
corepack yarn typecheck
corepack yarn test
Use the current React Native stack when checking package versions, and pair tests with the release checklist before shipping.
What to Test First
Start with code that can break revenue, trust, or core usage:
- authentication helpers and form validation;
- checkout, booking, subscription, or payment state;
- chat send/receive UI states;
- media upload error handling;
- location permission branches;
- reducers, selectors, and formatting helpers;
- screens with loading, empty, error, and success states.
Do not aim for a percentage first. Aim for the flows that would hurt users if they regressed.
Unit Tests vs Component Tests
Use unit tests for pure logic:
export function formatUnreadCount(count: number) {
if (count <= 0) return '';
if (count > 99) return '99+';
return String(count);
}
import { formatUnreadCount } from './formatUnreadCount';
describe('formatUnreadCount', () => {
it('hides zero unread messages', () => {
expect(formatUnreadCount(0)).toBe('');
});
it('caps large counts', () => {
expect(formatUnreadCount(128)).toBe('99+');
});
});
Use component tests when the behavior is visible through UI:
import { useState } from 'react';
import { Button, Text, TextInput, View } from 'react-native';
export function PromoCodeForm({ onApply }: { onApply: (code: string) => void }) {
const [code, setCode] = useState('');
return (
<View>
<TextInput
accessibilityLabel="Promo code"
value={code}
onChangeText={setCode}
/>
<Button title="Apply" onPress={() => onApply(code.trim())} />
{code.length === 0 ? <Text>Enter a promo code</Text> : null}
</View>
);
}
import { fireEvent, render, screen } from '@testing-library/react-native';
import { PromoCodeForm } from './PromoCodeForm';
it('applies a trimmed promo code', () => {
const onApply = jest.fn();
render(<PromoCodeForm onApply={onApply} />);
fireEvent.changeText(screen.getByLabelText('Promo code'), ' SAVE10 ');
fireEvent.press(screen.getByText('Apply'));
expect(onApply).toHaveBeenCalledWith('SAVE10');
});
This test does not inspect component state. It verifies what a user can do.
Mega Bundle Sale is ON! Get ALL of our React Native codebases at 90% OFF discount 🔥
Get the Mega BundleMock Native Modules Only When Needed
React Native component tests run in Node.js. They do not execute iOS or Android native code. Mock native modules when the JavaScript test depends on camera, location, push notifications, secure storage, analytics, or payment SDKs.
Keep mocks close to the dependency boundary:
jest.mock('expo-local-authentication', () => ({
hasHardwareAsync: jest.fn(async () => true),
isEnrolledAsync: jest.fn(async () => true),
authenticateAsync: jest.fn(async () => ({ success: true })),
}));
Mocking every dependency makes tests less useful. If a helper can be tested with real inputs and outputs, test it directly.
Test Async UI States
Most production React Native bugs happen around async state: a request fails, a listener returns an empty array, a permission is denied, or a loading spinner never resolves.
import { render, screen, waitFor } from '@testing-library/react-native';
import { ConversationList } from './ConversationList';
it('shows an empty state when there are no conversations', async () => {
render(<ConversationList loadConversations={async () => []} />);
expect(screen.getByText('Loading conversations')).toBeTruthy();
await waitFor(() => {
expect(screen.getByText('No conversations yet')).toBeTruthy();
});
});
For app screens, keep loading, empty, error, and success states separate. The code snippets guide has examples for this pattern.
Be Careful With Snapshots
Snapshots can catch unexpected changes, but large snapshots are hard to review. Use them only for small, stable components. Prefer explicit assertions when a screen has forms, permissions, navigation, or async data.
Good assertions answer questions like:
- "Is the error message visible?"
- "Does pressing Save call the submit handler?"
- "Does an empty list render the empty state?"
- "Does the disabled button stay disabled until required fields are valid?"
Add Tests to Release Gates
For a production React Native app, tests should run alongside typecheck and build validation:
corepack yarn typecheck
corepack yarn test --runInBand
corepack yarn react-native config
Unit and component tests do not replace device QA. They catch JavaScript and UI regressions before a release candidate reaches TestFlight, Google Play testing tracks, or Firebase App Distribution.
Looking for a custom mobile application?
Our team of expert mobile developers can help you build a custom mobile app that meets your specific needs.
Get in TouchFAQ
Is Jest enough for a React Native app?
Jest is the base test runner, but most UI behavior is easier to test with React Native Testing Library. Use Jest for pure logic and Testing Library for components.
Should I test internal state?
Usually no. Test what the user can see or do. State is an implementation detail and can change during refactors even when behavior stays correct.
Do unit tests catch native bugs?
No. JavaScript tests do not prove that iOS or Android native code works. Use real-device smoke tests and release builds for native behavior.