Module 10: Testing & Debugging

Learn comprehensive testing strategies and debugging techniques for React Native apps.

Back to Course|4 hours|Advanced

Testing & Debugging

Learn comprehensive testing strategies and debugging techniques for React Native apps.

Progress: 0/4 topics completed0%

Select Topics Overview

Unit Testing with Jest

Learn to write unit tests for React Native components and functions using Jest testing framework

Content by: Jigar Solanki

React Native Developer

Connect

Jest Setup

Set up Jest testing environment for React Native applications with proper configuration and mocking capabilities.

Jest Configuration

  • โ€ขInstall Jest and React Native testing dependencies
  • โ€ขConfigure jest.config.js for React Native
  • โ€ขSet up test environment and presets
  • โ€ขConfigure module name mapping
  • โ€ขSet up coverage reporting
  • โ€ขConfigure test timeout and setup files

Writing Tests

Write comprehensive unit tests for components, functions, and utilities with proper assertions and test coverage.

Comprehensive Test Example

Code Example
// utils/helpers.js
export const formatCurrency = (amount, currency = 'USD') => {
  if (typeof amount !== 'number') {
    throw new Error('Amount must be a number');
  }
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency,
  }).format(amount);
};

export const validateEmail = (email) => {
  const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
  return emailRegex.test(email);
};

export const debounce = (func, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(null, args), delay);
  };
};

// components/Button.js
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';

export const Button = ({ title, onPress, disabled = false, variant = 'primary' }) => {
  return (
    <TouchableOpacity
      style={[styles.button, styles[variant], disabled && styles.disabled]}
      onPress={onPress}
      disabled={disabled}
      testID="button"
    >
      <Text style={[styles.text, disabled && styles.disabledText]}>
        {title}
      </Text>
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  button: {
    paddingHorizontal: 20,
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  primary: {
    backgroundColor: '#2196F3',
  },
  secondary: {
    backgroundColor: '#6c757d',
  },
  disabled: {
    backgroundColor: '#e9ecef',
  },
  text: {
    color: 'white',
    fontWeight: 'bold',
  },
  disabledText: {
    color: '#6c757d',
  },
});

// __tests__/utils/helpers.test.js
import { formatCurrency, validateEmail, debounce } from '../utils/helpers';

describe('formatCurrency', () => {
  it('should format currency correctly for USD', () => {
    expect(formatCurrency(100)).toBe('$100.00');
    expect(formatCurrency(1000)).toBe('$1,000.00');
    expect(formatCurrency(99.99)).toBe('$99.99');
  });

  it('should format currency for different currencies', () => {
    expect(formatCurrency(100, 'EUR')).toBe('โ‚ฌ100.00');
    expect(formatCurrency(100, 'GBP')).toBe('ยฃ100.00');
  });

  it('should throw error for invalid input', () => {
    expect(() => formatCurrency('invalid')).toThrow('Amount must be a number');
    expect(() => formatCurrency(null)).toThrow('Amount must be a number');
    expect(() => formatCurrency(undefined)).toThrow('Amount must be a number');
  });

  it('should handle edge cases', () => {
    expect(formatCurrency(0)).toBe('$0.00');
    expect(formatCurrency(-100)).toBe('-$100.00');
  });
});

describe('validateEmail', () => {
  it('should validate correct email addresses', () => {
    expect(validateEmail('test@example.com')).toBe(true);
    expect(validateEmail('user.name@domain.co.uk')).toBe(true);
    expect(validateEmail('user+tag@example.org')).toBe(true);
  });

  it('should reject invalid email addresses', () => {
    expect(validateEmail('invalid-email')).toBe(false);
    expect(validateEmail('@example.com')).toBe(false);
    expect(validateEmail('test@')).toBe(false);
    expect(validateEmail('')).toBe(false);
    expect(validateEmail(null)).toBe(false);
    expect(validateEmail(undefined)).toBe(false);
  });
});

describe('debounce', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('should debounce function calls', () => {
    const mockFn = jest.fn();
    const debouncedFn = debounce(mockFn, 100);

    debouncedFn('arg1');
    debouncedFn('arg2');
    debouncedFn('arg3');

    expect(mockFn).not.toHaveBeenCalled();

    jest.advanceTimersByTime(100);

    expect(mockFn).toHaveBeenCalledTimes(1);
    expect(mockFn).toHaveBeenCalledWith('arg3');
  });

  it('should reset timer on new calls', () => {
    const mockFn = jest.fn();
    const debouncedFn = debounce(mockFn, 100);

    debouncedFn('first');
    jest.advanceTimersByTime(50);
    debouncedFn('second');
    jest.advanceTimersByTime(50);

    expect(mockFn).not.toHaveBeenCalled();

    jest.advanceTimersByTime(50);

    expect(mockFn).toHaveBeenCalledTimes(1);
    expect(mockFn).toHaveBeenCalledWith('second');
  });
});

// __tests__/components/Button.test.js
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { Button } from '../components/Button';

describe('Button Component', () => {
  it('should render correctly with title', () => {
    const { getByText, getByTestId } = render(
      <Button title="Test Button" onPress={() => {}} />
    );

    expect(getByText('Test Button')).toBeTruthy();
    expect(getByTestId('button')).toBeTruthy();
  });

  it('should call onPress when pressed', () => {
    const mockOnPress = jest.fn();
    const { getByTestId } = render(
      <Button title="Test Button" onPress={mockOnPress} />
    );

    fireEvent.press(getByTestId('button'));

    expect(mockOnPress).toHaveBeenCalledTimes(1);
  });

  it('should not call onPress when disabled', () => {
    const mockOnPress = jest.fn();
    const { getByTestId } = render(
      <Button title="Test Button" onPress={mockOnPress} disabled={true} />
    );

    fireEvent.press(getByTestId('button'));

    expect(mockOnPress).not.toHaveBeenCalled();
  });

  it('should apply correct styles for different variants', () => {
    const { getByTestId, rerender } = render(
      <Button title="Primary" onPress={() => {}} variant="primary" />
    );

    let button = getByTestId('button');
    expect(button.props.style).toContainEqual(
      expect.objectContaining({ backgroundColor: '#2196F3' })
    );

    rerender(<Button title="Secondary" onPress={() => {}} variant="secondary" />);
    button = getByTestId('button');
    expect(button.props.style).toContainEqual(
      expect.objectContaining({ backgroundColor: '#6c757d' })
    );
  });

  it('should apply disabled styles when disabled', () => {
    const { getByTestId } = render(
      <Button title="Disabled" onPress={() => {}} disabled={true} />
    );

    const button = getByTestId('button');
    expect(button.props.style).toContainEqual(
      expect.objectContaining({ backgroundColor: '#e9ecef' })
    );
  });

  it('should handle missing onPress gracefully', () => {
    const { getByTestId } = render(
      <Button title="No Press" />
    );

    expect(() => {
      fireEvent.press(getByTestId('button'));
    }).not.toThrow();
  });
});

// jest.config.js
module.exports = {
  preset: 'react-native',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  transformIgnorePatterns: [
    'node_modules/(?!(react-native|@react-native|react-native-.*|@react-navigation|@react-native-community|@react-native-picker|@react-native-async-storage|@react-native-vector-icons)/)',
  ],
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/__tests__/**',
    '!src/**/node_modules/**',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  testEnvironment: 'jsdom',
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

// jest.setup.js
import 'react-native-gesture-handler/jestSetup';

// Mock react-native-reanimated
jest.mock('react-native-reanimated', () => {
  const Reanimated = require('react-native-reanimated/mock');
  Reanimated.default.call = () => {};
  return Reanimated;
});

// Mock react-native-vector-icons
jest.mock('react-native-vector-icons/MaterialIcons', () => 'Icon');

// Silence the warning: Animated: `useNativeDriver` is not supported
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
Swipe to see more code

๐ŸŽฏ Practice Exercise

Test your understanding of this topic:

Ready for the Next Module?

Continue your learning journey and master the next set of concepts.

Continue to Module 11