A Complete Guide to Unit Testing React Native

Master Jest, React Native Testing Library, and best practices for building reliable, tested React Native applications

Why Unit Testing Matters in React Native

Unit testing is a fundamental practice in React Native development that ensures your code works correctly, catches bugs early, and maintains code quality as your application grows. As your codebase expands, small errors and edge cases you don't expect can cascade into larger failures. Bugs lead to bad user experience and ultimately, business losses. One way to prevent fragile programming is to test your code before releasing it into the wild.

Testing is important because it helps you uncover mistakes and verifies that your code is working correctly. Perhaps even more importantly, testing ensures that your code continues to work in the future as you add new features, refactor existing ones, or upgrade major dependencies of your project. As documented in the React Native Testing Overview, comprehensive testing is essential for maintaining code quality over time.

One of the best ways to fix a bug in your code is to write a failing test that exposes it. Then when you fix the bug and re-run the test, if it passes it means the bug is fixed and has never been reintroduced into the codebase. Tests can also serve as documentation for new people joining your team, helping them understand how the existing code works.

Our mobile app development services emphasize test-driven development practices that deliver production-ready code from day one. Combined with our quality assurance services, we ensure comprehensive test coverage across your entire application stack.

Understanding the Testing Pyramid

The testing pyramid provides a framework for organizing your testing strategy. At the base are unit tests, which are fast and numerous. Above those are integration tests, and at the top are end-to-end tests, which are slower but provide higher confidence.

Types of Tests in React Native

Unit Tests cover the smallest parts of code, like individual functions or classes. When the object being tested has any dependencies, you'll often need to mock them out. The great thing about unit tests is that they are quick to write and run, providing fast feedback as you work. Jest even has a watch mode option to continuously run tests that are related to code you're editing. As the React Native Testing Overview notes, unit tests form the foundation of a robust testing strategy.

Integration Tests combine several modules of your app and test them together to ensure their cooperation works as expected. You'll still need mocks for external services, but much less than in unit testing.

Component Tests focus on React components, which are responsible for rendering your app. Users directly interact with component output, so without component tests you may still deliver a broken UI to users even if business logic has high testing coverage.

End-to-End Tests verify your app is working as expected on a device or simulator from the user perspective. You build your app in release configuration and run tests against it.

For complex mobile applications, our full-stack development services incorporate comprehensive testing strategies that balance all test types for optimal coverage and performance.

Jest Setup for React Native

Everything you need to configure Jest for your React Native project

Default Configuration

React Native 0.38+ includes Jest preset by default. Simply add the preset to your package.json and run tests with npm test.

Custom Transform Patterns

Configure transformIgnorePatterns to transpile npm modules that don't ship pre-compiled code for React Native.

Module Mapping

Use moduleNameMapper to resolve module paths, mock images, and handle alias configurations in your tests.

Setup Files

Specify setup scripts that run before each test file for consistent global configuration and mocks.

package.json Jest Configuration
1{2 "scripts": {3 "test": "jest"4 },5 "jest": {6 "preset": "react-native",7 "transformIgnorePatterns": [8 "node_modules/(?!(@react-native|react-native|my-project)/)"9 ],10 "moduleNameMapper": {11 "^@components/(.*)$": "<rootDir>/src/components/$1"12 }13 }14}

Writing Your First Unit Test

Unit tests should be short and test only one thing. We'll use the AAA pattern: Arrange, Act, Assert. Following the patterns outlined in the React Native Testing Overview ensures tests are structured for maximum clarity and maintainability.

// utils/dateUtils.js
export function colorForDueDate(dateString) {
 const date = new Date(dateString);
 const today = new Date();

 if (date < today) {
 return 'red';
 } else if (date.getTime() === today.getTime()) {
 return 'yellow';
 }
 return 'green';
}

// utils/__tests__/dateUtils.test.js
import { colorForDueDate } from '../dateUtils';

it('given a date in the past, colorForDueDate() returns red', () => {
 expect(colorForDueDate('2000-10-20')).toBe('red');
});

it('given today\'s date, colorForDueDate() returns yellow', () => {
 const today = new Date().toISOString().split('T')[0];
 expect(colorForDueDate(today)).toBe('yellow');
});

it('given a future date, colorForDueDate() returns green', () => {
 expect(colorForDueDate('2099-12-31')).toBe('green');
});

The test description follows the pattern: given some precondition, when some action is executed, then the expected outcome. This makes tests self-documenting and easier to understand.

Organizing Tests with Describe

Jest offers the describe function to help structure your tests. Use describe to group together all tests that belong to one functionality. Describes can be nested if needed. As recommended in the React Native Testing Overview, well-organized test suites improve maintainability and readability.

describe('colorForDueDate', () => {
 describe('past dates', () => {
 it('returns red for dates before today', () => {
 expect(colorForDueDate('2000-10-20')).toBe('red');
 });
 });

 describe('today', () => {
 it('returns yellow for today\'s date', () => {
 const today = new Date().toISOString().split('T')[0];
 expect(colorForDueDate(today)).toBe('yellow');
 });
 });

 describe('future dates', () => {
 it('returns green for future dates', () => {
 expect(colorForDueDate('2099-12-31')).toBe('green');
 });
 });
});

Using Setup Functions

Common setup functions include beforeEach and beforeAll for setting up objects you're testing. For enterprise React Native applications, our software development services incorporate these testing patterns at scale across large codebases.

Mocking Dependencies in Tests

Sometimes when your tested objects have external dependencies, you'll want to mock them out. Mocking replaces some dependency of your code with your own implementation. According to the React Native Testing Overview, proper mocking strategies are essential for reliable, fast test execution.

Generally, using real objects in tests is better than mocks, but there are situations where this is not possible, such as when your test relies on a native module written in Java or Objective-C.

Why Mock External Dependencies

Consider an app that shows the current weather using an external service. You don't want to call that service in your tests because it could make tests slow and unstable due to network requests, the service may return different data every time you run tests, and third-party services can go offline when you really need to run tests. The React Native Testing Overview emphasizes isolating tests from external dependencies for consistency.

Our testing approach in mobile app development projects strategically mocks external dependencies while preserving test value and reliability.

Mocking API Calls in Tests
1// weatherService.test.js2import { getWeather } from '../weatherService';3 4jest.mock('../weatherService', () => ({5 getWeather: jest.fn(),6}));7 8describe('WeatherDisplay', () => {9 beforeEach(() => {10 jest.clearAllMocks();11 });12 13 it('displays weather information', async () => {14 getWeather.mockResolvedValue({15 temperature: 72,16 condition: 'sunny',17 city: 'San Francisco'18 });19 20 const weather = await getWeather('San Francisco');21 22 expect(weather.temperature).toBe(72);23 expect(weather.condition).toBe('sunny');24 });25});

Mocking Native Modules

React Native components or third-party components that rely on native code need special mocking. The Jest preset comes with default mocks, but for native modules you'll use Jest's manual mocking system. The Jest Tutorial for React Native Apps provides detailed guidance on configuring mocks for native dependencies.

// __mocks__/react-native-video.js
export default 'Video';

// Or for a more complex mock that forwards props
jest.mock('react-native-video', () => {
 const mockComponent = require('react-native/jest/mockComponent');
 return mockComponent('react-native-video');
});

Proper native module mocking is essential when testing cross-platform mobile applications that leverage device-specific functionality.

Component Testing with React Native Testing Library

React Native Testing Library builds on top of React's test renderer and provides APIs for firing events and querying component output. The library encourages testing from the user perspective rather than implementation details. The React Native Testing Overview emphasizes this user-centric approach as a best practice.

When testing user interactions, test the component from the user perspective--what's on the page? What changes when interacted with? As a rule of thumb, prefer using things users can see or hear: make assertions using rendered text or accessibility helpers. The React Native Testing Library documentation provides comprehensive guidance on querying strategies.

Avoid making assertions on component props or internal state, and avoid testID queries. Testing implementation details tends to break by refactoring when you rename things or rewrite class components using hooks.

For enterprise mobile applications, our quality assurance methodology incorporates component testing best practices that ensure UI reliability across all device types and screen sizes.

Testing Component User Interactions
1// components/__tests__/GroceryShoppingList.test.tsx2import { render, fireEvent } from '@testing-library/react-native';3import { GroceryShoppingList } from '../GroceryShoppingList';4 5test('given empty GroceryShoppingList, user can add an item to it', () => {6 const { getByPlaceholderText, getByText, getAllByText } = render(7 <GroceryShoppingList />8 );9 10 fireEvent.changeText(11 getByPlaceholderText('Enter grocery item'),12 'banana'13 );14 fireEvent.press(getByText('Add the item to list'));15 16 const bananaElements = getAllByText('banana');17 expect(bananaElements).toHaveLength(1);18});

Snapshot Testing in React Native

Snapshot testing is an advanced kind of testing enabled by Jest. A component snapshot is a textual representation of your component's render output generated during a test run. As described in the React Native Testing Overview, snapshot testing provides an additional layer of protection against unexpected UI changes.

// components/__tests__/Intro.test.tsx
import React from 'react';
import renderer from 'react-test-renderer';
import Intro from '../Intro';

test('renders correctly', () => {
 const tree = renderer.create(<Intro />).toJSON();
 expect(tree).toMatchSnapshot();
});

When to Use Snapshot Testing

Snapshots are good at guarding against unexpected changes and checking that components receive expected props. However, they have several weak points. It can be hard to tell whether a change in snapshot is intended or evidence of a bug, especially with large snapshots. When a snapshot is created, it's considered correct even if the rendered output is actually wrong. The React Native Testing Overview recommends using snapshots judiciously and combining them with explicit behavioral tests.

We recommend using small snapshots only. If you want to test a change between two React component states, use snapshot-diff. When in doubt, prefer explicit expectations as described in the component testing section.

Testing Hooks and State Management

Custom hooks and state management solutions require specific testing approaches to ensure they're functioning correctly.

Testing Custom Hooks

// hooks/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react-native';
import { useCounter } from '../useCounter';

test('initializes with zero', () => {
 const { result } = renderHook(() => useCounter());
 expect(result.current.count).toBe(0);
});

test('increments count', () => {
 const { result } = renderHook(() => useCounter());
 act(() => {
 result.current.increment();
 });
 expect(result.current.count).toBe(1);
});

Testing Redux Selectors and Reducers

describe('todos reducer', () => {
 it('should return initial state', () => {
 expect(todosReducer(undefined, {})).toEqual([]);
 });

 it('should add a todo', () => {
 const action = addTodo('Learn React Native');
 expect(todosReducer([], action)).toEqual([
 { id: 1, text: 'Learn React Native', completed: false }
 ]);
 });
});

State management testing is critical for complex React Native applications that require predictable data flow across multiple components and screens.

Common Testing Pitfalls and Solutions

Testing Implementation Details

Avoid testing implementation details like internal state, props, or event handlers. Such tests work but aren't oriented toward how users interact with components and break easily during refactoring.

Problematic:

// Bad: Testing internal implementation
expect(wrapper.state('count')).toBe(5);

Better:

// Good: Testing user-visible behavior
const { getByText } = render(<Counter />);
fireEvent.press(getByText('Increment'));
expect(getByText('5')).toBeTruthy();

Flaky Tests

Flaky tests randomly pass and fail without any code change. Common causes include timing issues, network dependencies, and shared state between tests.

Solutions:

  • Use fake timers for time-dependent code
  • Mock external API calls
  • Ensure each test is independent
  • Clean up mocks and state in afterEach

Our quality assurance process identifies and eliminates flaky tests to ensure reliable CI/CD pipelines for all client projects.

Best Practices for Unit Testing

Write User-Centric Tests

Test what users see and do, not internal implementation. Use React Native Testing Library to query by text and accessibility labels.

Follow AAA Pattern

Arrange your test setup, Act by performing the action being tested, Assert by verifying the expected outcome.

Keep Tests Independent

Each test should run independently without relying on other tests. Avoid shared state that could cause interference.

Mock Wisely

Only mock external dependencies, native modules, and non-deterministic systems. Don't mock so much tests become meaningless.

Integrating Tests into Your Development Workflow

Running Tests During Development

Use Jest's watch mode to run tests related to files you're editing:

npm test -- --watch

This provides immediate feedback when you break existing functionality.

Pre-commit Hooks

Use Husky or similar tools to run tests before commits:

{
 "husky": {
 "hooks": {
 "pre-commit": "npm test"
 }
 }
}

CI/CD Integration

Run tests in your continuous integration pipeline to catch issues before deployment. Our DevOps and CI/CD services integrate automated testing into your deployment pipeline for consistent quality assurance.

Coverage Reports

npm test -- --coverage

Coverage reports help identify untested areas but focus on meaningful coverage rather than arbitrary percentages. Aim for comprehensive testing of business logic and critical user paths.

Conclusion

Unit testing is an essential practice for building reliable React Native applications. By following the testing pyramid, setting up Jest properly, and writing user-centric tests with React Native Testing Library, you can create a test suite that catches bugs early, enables confident refactoring, and serves as living documentation for your codebase.

Start with unit tests for your business logic and utilities, then add component tests for critical UI interactions. As your application matures, consider integration and end-to-end tests for complex user flows. The investment in testing pays dividends through reduced bugs, faster development cycles, and higher confidence in your releases.

Remember: the goal isn't 100% coverage, but rather a suite of meaningful tests that verify your application works as users expect. Test behavior, not implementation, and your tests will remain valuable even as your code evolves.

For comprehensive React Native development with built-in testing excellence, explore our mobile app development services or contact our team to discuss your project requirements.

Frequently Asked Questions

Ready to Build Tested React Native Apps?

Our team specializes in creating robust, well-tested mobile applications using React Native and modern testing practices.

Sources

  1. React Native Testing Overview - Official comprehensive testing guide covering all testing types and best practices
  2. Jest Tutorial: Testing React Native Apps - Official Jest documentation for React Native setup and configuration
  3. Expo Unit Testing Documentation - Expo-specific unit testing setup and best practices
  4. React Native Testing Library Documentation - User-centric testing approach documentation