Back to Posts

Testing React Native Apps - A Comprehensive Guide

By Lumina Software
react-nativetestingmobile-developmentbest-practices

Testing React Native Apps - A Comprehensive Guide

Testing React Native apps requires a different approach than web apps. You're dealing with native modules, platform-specific code, and real device behavior. Here's how to build a comprehensive testing strategy.

Testing Pyramid

Unit Tests (Foundation)

Test individual functions and components in isolation:

// utils/formatDate.test.ts
import { formatDate } from './formatDate';

describe('formatDate', () => {
  it('formats date correctly', () => {
    const date = new Date('2025-10-26');
    expect(formatDate(date)).toBe('Oct 26, 2025');
  });
  
  it('handles invalid dates', () => {
    expect(() => formatDate(null)).toThrow();
  });
});

Integration Tests (Middle Layer)

Test how components work together:

// components/TaskList.test.tsx
import { render, screen } from '@testing-library/react-native';
import { TaskList } from './TaskList';

describe('TaskList', () => {
  it('renders tasks', () => {
    const tasks = [
      { id: '1', title: 'Task 1', completed: false },
      { id: '2', title: 'Task 2', completed: true },
    ];
    
    render(<TaskList tasks={tasks} />);
    
    expect(screen.getByText('Task 1')).toBeTruthy();
    expect(screen.getByText('Task 2')).toBeTruthy();
  });
  
  it('handles empty state', () => {
    render(<TaskList tasks={[]} />);
    expect(screen.getByText('No tasks')).toBeTruthy();
  });
});

E2E Tests (Top Layer)

Test complete user flows:

// e2e/taskFlow.e2e.ts
describe('Task Flow', () => {
  it('creates and completes a task', async () => {
    await element(by.id('add-task-button')).tap();
    await element(by.id('task-input')).typeText('New Task');
    await element(by.id('save-button')).tap();
    
    await expect(element(by.text('New Task'))).toBeVisible();
    
    await element(by.id('task-checkbox')).tap();
    await expect(element(by.id('completed-indicator'))).toBeVisible();
  });
});

Testing Setup

Jest Configuration

// jest.config.js
module.exports = {
  preset: 'react-native',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  transformIgnorePatterns: [
    'node_modules/(?!(react-native|@react-native|expo)/)',
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/__tests__/**',
  ],
};

Testing Library Setup

// jest.setup.js
import '@testing-library/jest-native/extend-expect';

// Mock React Native modules
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

Component Testing

Basic Component Test

import { render, screen, fireEvent } from '@testing-library/react-native';
import { Button } from './Button';

describe('Button', () => {
  it('renders with title', () => {
    render(<Button title="Click me" onPress={() => {}} />);
    expect(screen.getByText('Click me')).toBeTruthy();
  });
  
  it('calls onPress when pressed', () => {
    const onPress = jest.fn();
    render(<Button title="Click me" onPress={onPress} />);
    
    fireEvent.press(screen.getByText('Click me'));
    expect(onPress).toHaveBeenCalledTimes(1);
  });
  
  it('is disabled when disabled prop is true', () => {
    render(<Button title="Click me" onPress={() => {}} disabled />);
    const button = screen.getByText('Click me');
    expect(button).toBeDisabled();
  });
});

Testing Hooks

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

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

Testing Navigation

import { render } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

function renderWithNavigation(component: React.ReactElement) {
  return render(
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Test" component={() => component} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

describe('ProfileScreen', () => {
  it('navigates to settings', () => {
    const navigation = { navigate: jest.fn() };
    renderWithNavigation(<ProfileScreen navigation={navigation} />);
    
    fireEvent.press(screen.getByText('Settings'));
    expect(navigation.navigate).toHaveBeenCalledWith('Settings');
  });
});

API Testing

Mocking API Calls

// __mocks__/api.ts
export const fetchUser = jest.fn();
export const createPost = jest.fn();

// In test
import { fetchUser } from '@/api';

jest.mock('@/api');

describe('UserProfile', () => {
  it('loads user data', async () => {
    fetchUser.mockResolvedValue({ id: '1', name: 'John' });
    
    render(<UserProfile userId="1" />);
    
    await waitFor(() => {
      expect(screen.getByText('John')).toBeTruthy();
    });
  });
});

Testing with TanStack Query

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react-native';

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } },
});

function wrapper({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

describe('useUser', () => {
  it('fetches user data', async () => {
    fetchUser.mockResolvedValue({ id: '1', name: 'John' });
    
    const { result } = renderHook(() => useUser('1'), { wrapper });
    
    await waitFor(() => expect(result.current.isSuccess).toBe(true));
    expect(result.current.data.name).toBe('John');
  });
});

E2E Testing with Detox

Setup

npm install --save-dev detox
// .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/App.app',
      build: 'xcodebuild -workspace ios/App.xcworkspace -scheme App -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug'
    }
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: {
        type: 'iPhone 14'
      }
    },
    emulator: {
      type: 'android.emulator',
      device: {
        avdName: 'Pixel_4_API_30'
      }
    }
  }
};

E2E Test Example

// e2e/taskFlow.e2e.ts
describe('Task Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });
  
  beforeEach(async () => {
    await device.reloadReactNative();
  });
  
  it('should create a task', async () => {
    await element(by.id('add-task-button')).tap();
    await element(by.id('task-input')).typeText('New Task');
    await element(by.id('save-button')).tap();
    
    await expect(element(by.text('New Task'))).toBeVisible();
  });
  
  it('should complete a task', async () => {
    await element(by.id('task-1')).tap();
    await expect(element(by.id('completed-indicator'))).toBeVisible();
  });
});

Snapshot Testing

Component Snapshots

import renderer from 'react-test-renderer';

describe('Button', () => {
  it('matches snapshot', () => {
    const tree = renderer
      .create(<Button title="Click me" onPress={() => {}} />)
      .toJSON();
    expect(tree).toMatchSnapshot();
  });
});

Use sparingly: Snapshots break easily and can become maintenance burden.

Performance Testing

Measuring Render Times

import { performance } from 'perf_hooks';

describe('Performance', () => {
  it('renders list quickly', () => {
    const start = performance.now();
    render(<TaskList tasks={largeTaskList} />);
    const duration = performance.now() - start;
    
    expect(duration).toBeLessThan(100); // 100ms threshold
  });
});

Platform-Specific Testing

Testing Platform Differences

import { Platform } from 'react-native';

describe('Platform-specific behavior', () => {
  it('renders differently on iOS', () => {
    Platform.OS = 'ios';
    const { container } = render(<Component />);
    expect(container).toHaveStyle({ paddingTop: 20 });
  });
  
  it('renders differently on Android', () => {
    Platform.OS = 'android';
    const { container } = render(<Component />);
    expect(container).toHaveStyle({ paddingTop: 0 });
  });
});

Testing Best Practices

  1. Test behavior, not implementation: Test what users see and do
  2. Keep tests simple: One assertion per test when possible
  3. Use descriptive names: Test names should explain what's tested
  4. Mock external dependencies: Don't rely on real APIs/databases
  5. Test edge cases: Empty states, errors, loading states
  6. Maintain test data: Use factories for test data
  7. Clean up: Reset state between tests

Test Data Factories

// factories/taskFactory.ts
export function createTask(overrides?: Partial<Task>): Task {
  return {
    id: '1',
    title: 'Test Task',
    completed: false,
    createdAt: new Date(),
    ...overrides,
  };
}

// In tests
const task = createTask({ title: 'Custom Task' });

Coverage Goals

Aim for:

  • 80%+ overall coverage
  • 100% coverage for critical paths
  • High coverage for utilities and helpers
  • Reasonable coverage for UI components

Continuous Integration

GitHub Actions Example

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

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
      - run: npm install
      - run: npm test -- --coverage
      - run: npm run test:e2e

Conclusion

A comprehensive testing strategy includes:

  • Unit tests: Fast, isolated tests
  • Integration tests: Component interactions
  • E2E tests: Complete user flows
  • Performance tests: Ensure responsiveness
  • Platform tests: Verify platform differences

Test early, test often, and test what matters. Good tests give you confidence to refactor and ship features quickly.