TestingQuality AssuranceReact Native

React Native Testing Strategies: From Unit Tests to E2E Testing

By Viewlytics Team10 min readMay 10, 2025

Comprehensive testing is essential for building reliable React Native applications. This guide covers the complete testing pyramid from unit tests to end-to-end testing, including setup, best practices, and CI/CD integration.

1. Testing Pyramid Overview

A well-structured testing strategy follows the testing pyramid principle:

Unit Tests

70% of tests

Fast, isolated, component logic
Integration Tests

20% of tests

Component interactions
E2E Tests

10% of tests

Full user workflows

2. Unit Testing with Jest

Jest Setup

Configure Jest for React Native testing:

// jest.config.js
module.exports = {
  preset: 'react-native',
  setupFilesAfterEnv: [
    '<rootDir>/src/test-utils/setup.js'
  ],
  transformIgnorePatterns: [
    'node_modules/(?!(react-native|@react-native|react-clone-referenced-element|@react-navigation)/)'
  ],
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/test-utils/**',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Testing Components

Test React Native components with React Native Testing Library:

import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { UserProfile } from '../UserProfile';

describe('UserProfile', () => {
  const mockUser = {
    id: '1',
    name: 'John Doe',
    email: 'john@example.com'
  };

  it('renders user information correctly', () => {
    const { getByText, getByTestId } = render(
      <UserProfile user={mockUser} />
    );

    expect(getByText('John Doe')).toBeTruthy();
    expect(getByText('john@example.com')).toBeTruthy();
    expect(getByTestId('user-avatar')).toBeTruthy();
  });

  it('calls onEdit when edit button is pressed', () => {
    const mockOnEdit = jest.fn();
    const { getByText } = render(
      <UserProfile user={mockUser} onEdit={mockOnEdit} />
    );

    fireEvent.press(getByText('Edit'));
    expect(mockOnEdit).toHaveBeenCalledWith(mockUser);
  });
});

3. Testing Custom Hooks

Hook Testing Strategy

Use @testing-library/react-hooks for testing custom hooks:

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

describe('useCounter', () => {
  it('should initialize with 0', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

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

  it('should decrement count', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(4);
  });
});

4. Mocking External Dependencies

API Mocking

Mock API calls and external services:

// __mocks__/api.js
export const api = {
  getUser: jest.fn(),
  updateUser: jest.fn(),
  deleteUser: jest.fn(),
};

// UserService.test.js
import { api } from '../api';
import { UserService } from '../UserService';

jest.mock('../api');

describe('UserService', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should fetch user data', async () => {
    const mockUser = { id: '1', name: 'John' };
    api.getUser.mockResolvedValue(mockUser);

    const result = await UserService.fetchUser('1');
    
    expect(api.getUser).toHaveBeenCalledWith('1');
    expect(result).toEqual(mockUser);
  });

  it('should handle API errors', async () => {
    api.getUser.mockRejectedValue(new Error('Network error'));

    await expect(UserService.fetchUser('1'))
      .rejects.toThrow('Network error');
  });
});

React Navigation Mocking

Mock navigation for testing components that use navigation:

// test-utils/navigation-mock.js
export const mockNavigation = {
  navigate: jest.fn(),
  goBack: jest.fn(),
  push: jest.fn(),
  pop: jest.fn(),
  reset: jest.fn(),
};

// Component.test.js
import { mockNavigation } from '../test-utils/navigation-mock';

describe('NavigationComponent', () => {
  it('navigates to details on button press', () => {
    const { getByText } = render(
      <NavigationComponent navigation={mockNavigation} />
    );

    fireEvent.press(getByText('Go to Details'));
    
    expect(mockNavigation.navigate).toHaveBeenCalledWith('Details', {
      id: '123'
    });
  });
});

5. E2E Testing with Detox

Detox Setup

Configure Detox for end-to-end testing:

// .detoxrc.js
module.exports = {
  testRunner: {
    args: {
      '$0': 'jest',
      config: 'e2e/jest.config.js'
    },
    jest: {
      setupTimeout: 120000
    }
  },
  apps: {
    'ios.debug': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YourApp.app'
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk'
    }
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: {
        type: 'iPhone 14'
      }
    },
    emulator: {
      type: 'android.emulator',
      device: {
        avdName: 'Pixel_4_API_30'
      }
    }
  },
  configurations: {
    'ios.sim.debug': {
      device: 'simulator',
      app: 'ios.debug'
    },
    'android.emu.debug': {
      device: 'emulator',
      app: 'android.debug'
    }
  }
};

E2E Test Examples

Write comprehensive end-to-end tests:

// e2e/login.e2e.js
describe('Login Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should login successfully with valid credentials', async () => {
    await element(by.id('email-input')).typeText('user@example.com');
    await element(by.id('password-input')).typeText('password123');
    await element(by.id('login-button')).tap();

    await waitFor(element(by.id('home-screen')))
      .toBeVisible()
      .withTimeout(5000);

    await expect(element(by.text('Welcome back!'))).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')).tap();

    await waitFor(element(by.text('Invalid credentials')))
      .toBeVisible()
      .withTimeout(3000);
  });
});

6. CI/CD Integration

GitHub Actions Setup

Automate testing in your CI/CD pipeline:

# .github/workflows/test.yml
name: Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm test -- --coverage --watchAll=false
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

  e2e-test:
    runs-on: macos-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Setup iOS Simulator
      run: |
        xcrun simctl create iPhone14 com.apple.CoreSimulator.SimDeviceType.iPhone-14
        xcrun simctl boot iPhone14
    
    - name: Build iOS app
      run: npx react-native run-ios --configuration Release --simulator
    
    - name: Run Detox tests
      run: npx detox test --configuration ios.sim.release

7. Testing Best Practices

✅ Do
  • • Test user behavior, not implementation
  • • Use testID for reliable element selection
  • • Mock external dependencies
  • • Keep tests isolated and independent
  • • Use descriptive test names
  • • Test error states and edge cases
❌ Don't
  • • Test implementation details
  • • Use shallow rendering for integration tests
  • • Make tests dependent on each other
  • • Ignore async behavior
  • • Test third-party library logic
  • • Write overly complex test setups

8. Test Coverage and Quality Metrics

Monitor your testing effectiveness with key metrics:

85%

Code Coverage

2.3s

Test Execution Time

156

Total Tests

98%

Pass Rate

Conclusion

A comprehensive testing strategy is essential for building reliable React Native applications. Start with unit tests for core logic, add integration tests for component interactions, and implement E2E tests for critical user journeys.

Remember that testing is an investment in code quality and developer confidence. Well-tested code is easier to refactor, debug, and maintain over time.

🚀 Ready to enhance your testing strategy?

Complement your testing pyramid with Viewlytics' AI-powered UI testing. Automatically capture screenshots across real devices and detect visual bugs, layout issues, and UI inconsistencies that traditional tests might miss.

Start Visual Testing with Viewlytics
logo

2025 © Viewlytics. All rights reserved.