Testing React Native Apps: Jest Unit Tests, Component Tests, and Detox E2E

14 min read
#React Native#Testing#Jest#Detox#Quality Assurance

Quality mobile apps are built on solid test foundations. Testing separates apps that work from apps that work reliably. React Native developers have powerful testing tools—Jest, React Native Testing Library, and Detox—that enable comprehensive coverage from unit tests to end-to-end scenarios. This guide covers professional testing strategies for production React Native apps.

Why Testing Matters in Mobile

Mobile apps face unique challenges: diverse devices, OS versions, network conditions, and real user scenarios. Testing catches these issues before users do.

Testing pyramid for React Native:

  • Unit tests (70%): Individual functions and hooks
  • Component tests (20%): UI logic and user interactions
  • E2E tests (10%): Critical user flows on real devices

This distribution maximizes coverage while maintaining reasonable test execution time.

Key Takeaways

  • Jest handles unit and component testing with excellent React mocking
  • React Native Testing Library lets you test components like users interact with them
  • Detox enables reliable E2E testing on real and emulated devices
  • Mock strategies for Firebase, async operations, and network calls are essential
  • Test organization (unit/component/E2E separation) maintains sustainability
  • CI/CD integration ensures tests run automatically on every push

Setting Up Jest for React Native

Jest is the standard test runner for React Native. It's pre-configured but requires key settings for mobile environments.

Jest Configuration

Create or update jest.config.js:

module.exports = {
  preset: 'react-native',
  testEnvironment: 'node',
  testMatch: ['**/__tests__/**/*.test.js', '**/*.test.js'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  collectCoverageFrom: [
    'src/**/*.js',
    'src/**/*.jsx',
    '!src/**/*.test.js',
    '!src/index.js',
  ],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  },
};

Jest Setup File

Create jest.setup.js for global mocks:

jest.mock('@react-native-async-storage/async-storage', () => ({
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn(),
}));

jest.mock('@react-native-community/netinfo', () => ({
  fetch: jest.fn().mockResolvedValue({
    isConnected: true,
    isInternetReachable: true,
  }),
  addEventListener: jest.fn(),
}));

global.fetch = jest.fn();

Unit Testing with Jest

Unit tests verify individual functions in isolation. They're fast, focused, and ideal for business logic.

Testing Utility Functions

export const calculateDiscount = (price, discountPercent) => {
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Invalid discount percentage');
  }
  return price * (1 - discountPercent / 100);
};

describe('calculateDiscount', () => {
  it('calculates correct discount for valid inputs', () => {
    expect(calculateDiscount(100, 20)).toBe(80);
    expect(calculateDiscount(50, 50)).toBe(25);
  });

  it('throws error for invalid discount', () => {
    expect(() => calculateDiscount(100, 150)).toThrow(
      'Invalid discount percentage'
    );
    expect(() => calculateDiscount(100, -10)).toThrow();
  });

  it('handles edge cases', () => {
    expect(calculateDiscount(0, 50)).toBe(0);
    expect(calculateDiscount(100, 0)).toBe(100);
    expect(calculateDiscount(100, 100)).toBe(0);
  });
});

Testing Custom Hooks

import {useState} from 'react';

export const useCounter = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);

  return {count, increment, decrement, reset};
};

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

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

  it('initializes with custom value', () => {
    const {result} = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('increments and decrements correctly', () => {
    const {result} = renderHook(() => useCounter(5));

    act(() => {
      result.current.increment();
    });
    expect(result.current.count).toBe(6);

    act(() => {
      result.current.decrement();
      result.current.decrement();
    });
    expect(result.current.count).toBe(4);
  });

  it('resets to initial value', () => {
    const {result} = renderHook(() => useCounter(100));

    act(() => {
      result.current.increment();
      result.current.increment();
    });

    act(() => {
      result.current.reset();
    });
    expect(result.current.count).toBe(100);
  });
});

Testing Async Operations

export const fetchUserData = async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error('Failed to fetch user');
  return response.json();
};

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

  it('fetches and returns user data', async () => {
    const mockUser = {id: 1, name: 'John', email: 'john@example.com'};
    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });

    const result = await fetchUserData(1);
    expect(result).toEqual(mockUser);
    expect(global.fetch).toHaveBeenCalledWith('/api/users/1');
  });

  it('throws error on failed fetch', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: false,
    });

    await expect(fetchUserData(1)).rejects.toThrow(
      'Failed to fetch user'
    );
  });
});

Component Testing with React Native Testing Library

Component tests verify UI logic and user interactions. They test components as users see them.

Basic Component Testing

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

const WelcomeButton = ({onPress, title}) => (
  <Button title={title} onPress={onPress} />
);

describe('WelcomeButton', () => {
  it('renders button with correct title', () => {
    render(<WelcomeButton title="Press Me" onPress={jest.fn()} />);
    expect(screen.getByRole('button', {name: 'Press Me'})).toBeTruthy();
  });

  it('calls onPress when button is pressed', () => {
    const mockPress = jest.fn();
    render(<WelcomeButton title="Click" onPress={mockPress} />);

    fireEvent.press(screen.getByRole('button'));
    expect(mockPress).toHaveBeenCalled();
  });
});

Testing with User Interactions

import {render, screen, fireEvent} from '@testing-library/react-native';
import {TextInput, View, Text} from 'react-native';

const LoginForm = ({onSubmit}) => {
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');

  const handleSubmit = () => {
    onSubmit({email, password});
  };

  return (
    <View>
      <TextInput
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        testID="email-input"
      />
      <TextInput
        placeholder="Password"
        secureTextEntry
        value={password}
        onChangeText={setPassword}
        testID="password-input"
      />
      <Button title="Login" onPress={handleSubmit} testID="login-button" />
    </View>
  );
};

describe('LoginForm', () => {
  it('captures email and password input', () => {
    const mockSubmit = jest.fn();
    render(<LoginForm onSubmit={mockSubmit} />);

    fireEvent.changeText(screen.getByTestID('email-input'), 'test@example.com');
    fireEvent.changeText(screen.getByTestID('password-input'), 'password123');
    fireEvent.press(screen.getByTestID('login-button'));

    expect(mockSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123',
    });
  });

  it('validates form before submission', () => {
    const mockSubmit = jest.fn();
    render(<LoginForm onSubmit={mockSubmit} />);

    fireEvent.press(screen.getByTestID('login-button'));

    expect(mockSubmit).toHaveBeenCalledWith({
      email: '',
      password: '',
    });
  });
});

Mocking Firebase

Firebase is common in React Native apps. Proper mocking enables testing without network calls:

jest.mock('@react-native-firebase/auth', () => ({
  auth: jest.fn(() => ({
    signInWithEmailAndPassword: jest.fn(),
    createUserWithEmailAndPassword: jest.fn(),
    signOut: jest.fn(),
    currentUser: null,
    onAuthStateChanged: jest.fn(),
  })),
}));

jest.mock('@react-native-firebase/database', () => ({
  database: jest.fn(() => ({
    ref: jest.fn(() => ({
      set: jest.fn(),
      once: jest.fn(),
      on: jest.fn(),
    })),
  })),
}));

describe('Auth with Firebase', () => {
  it('signs in user with email and password', async () => {
    const {signInWithEmailAndPassword} = require('@react-native-firebase/auth').auth();
    signInWithEmailAndPassword.mockResolvedValueOnce({
      user: {uid: '123', email: 'test@example.com'},
    });

    const result = await signInWithEmailAndPassword('test@example.com', 'password');
    expect(result.user.uid).toBe('123');
  });
});

End-to-End Testing with Detox

E2E tests run on real (or emulated) devices, testing complete user flows. Detox is the industry standard for React Native E2E testing.

Setting Up Detox

Install and configure Detox:

npm install detox-cli --global
npm install detox detox-cli --save-dev
detox init -r ios

Writing E2E Tests

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

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

  it('should sign in user with valid credentials', async () => {
    await waitFor(element(by.id('email-input')))
      .toBeVisible()
      .withTimeout(5000);

    await element(by.id('email-input')).typeText('test@example.com');
    await element(by.id('password-input')).typeText('password123');
    await element(by.id('login-button')).multiTap();

    await waitFor(element(by.text('Welcome')))
      .toBeVisible()
      .withTimeout(5000);
    await expect(element(by.text('Welcome'))).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')).multiTap();

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

  it('should navigate to details screen', async () => {
    await element(by.id('user-list-item')).atIndex(0).multiTap();

    await waitFor(element(by.text('User Details')))
      .toBeVisible()
      .withTimeout(5000);
  });
});

Detox Best Practices

describe('Complex User Flow', () => {
  it('navigates through multiple screens', async () => {
    await device.launchApp();

    await element(by.id('home-tab')).multiTap();
    await waitFor(element(by.text('Home')))
      .toBeVisible()
      .withTimeout(5000);

    await element(by.id('search-tab')).multiTap();
    await waitFor(element(by.text('Search Results')))
      .toBeVisible()
      .withTimeout(5000);

    await element(by.id('search-input')).typeText('React Native');
    await element(by.id('search-button')).multiTap();

    await waitFor(element(by.text('Results for: React Native')))
      .toBeVisible()
      .withTimeout(5000);
  });
});

Test Organization and Structure

Professional test suites are organized logically:

__tests__/
├── unit/
│   ├── utils/
│   │   ├── calculateDiscount.test.js
│   │   └── formatDate.test.js
│   ├── hooks/
│   │   └── useCounter.test.js
│   └── services/
│       └── api.test.js
├── components/
│   ├── LoginForm.test.js
│   ├── UserList.test.js
│   └── ProfileCard.test.js
├── integration/
│   ├── AuthFlow.test.js
│   └── UserProfile.test.js
└── e2e/
    ├── LoginFlow.e2e.js
    ├── Navigation.e2e.js
    └── DataSync.e2e.js

CI/CD Integration

Automated testing in CI/CD pipelines catches issues before production.

GitHub Actions Example

name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run unit and component tests
        run: npm run test

      - name: Upload coverage
        uses: codecov/codecov-action@v2
        with:
          files: ./coverage/lcov.info

      - name: Build app
        run: npm run build

Running Tests Locally

npm run test                    # Run all tests
npm run test -- --coverage     # With coverage report
npm run test -- --watch        # Watch mode
npm run test:e2e               # E2E tests only

Testing Best Practices

Unit Tests:

  • One assertion per test when possible
  • Test success cases and error scenarios
  • Mock external dependencies
  • Keep tests focused and fast

Component Tests:

  • Test user interactions, not implementation
  • Use accessible queries (by.text, by.id)
  • Test edge cases and error states
  • Avoid testing framework details

E2E Tests:

  • Test critical user flows only
  • Verify end results, not intermediate states
  • Use meaningful waits and timeouts
  • Test on multiple device configurations

General:

  • Aim for >70% code coverage
  • Write tests alongside code, not after
  • Keep tests maintainable and readable
  • Run tests frequently during development
  • Integrate tests into CI/CD pipeline

Common Pitfalls to Avoid

Flaky Tests:

  • Use explicit waits instead of arbitrary delays
  • Avoid testing implementation details
  • Mock network requests consistently

Slow Test Suites:

  • Keep unit tests fast (under 1ms per test)
  • Parallelize E2E tests
  • Cache dependencies

Poor Coverage:

  • Test behaviors, not lines of code
  • Focus on critical paths first
  • Avoid 100% coverage for diminishing returns

Testing is an investment that compounds. Initial effort in establishing testing infrastructure and habits pays dividends through fewer production bugs, faster refactoring, and confidence in deployments. By combining Jest unit tests, React Native Testing Library for components, and Detox for E2E flows, you build apps that work reliably across devices, OS versions, and real-world scenarios.


Let's bring your app idea to life

I specialize in mobile and backend development.

Share this article

Related Articles