6 min readLumina Software

Testing React Native Apps - A Comprehensive Guide

  • React native
  • Testing
  • Mobile development
  • Best practices

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.

Written by Lumina Software. Questions about anything here? Book a call

Keep reading

Tell us where the money leaks.

Book a call and we will walk through where an AI system could realistically move the numbers in your business. If it can't, we will say so.