Unit Testing with Jest
Learn to write unit tests for React Native components and functions using Jest testing framework. This is a foundational concept in cross-platform mobile development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world React Native experience. Take your time with each section and practice the examples
Jest Setup
Set up Jest testing environment for React Native applications with proper configuration and mocking capabilities.. This is an essential concept that every React Native developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Jest Configuration
- Install Jest and React Native testing dependencies
- Configure jest.config.js for React Native — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Set up test environment and presets — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Configure module name mapping — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Set up coverage reporting — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Configure test timeout and setup files — a critical concept in cross-platform mobile development that you will use frequently in real projects
Writing Tests
Write comprehensive unit tests for components, functions, and utilities with proper assertions and test coverage.. This is an essential concept that every React Native developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
comprehensive Test 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');