Component Testing
Test React Native components using React Testing Library for better component behavior testing. 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
React Testing Library
Use React Testing Library to test components in a way that resembles how users interact with your app.. 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
Testing Library Setup
- Install @testing-library/react-native and @testing-library/jest-native
- Configure custom render with providers — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Set up custom matchers for better assertions — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Create test utilities and helpers — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Mock navigation and context providers — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Set up accessibility testing — a critical concept in cross-platform mobile development that you will use frequently in real projects
Component Behavior
Test component behavior, user interactions, and state changes to ensure components work as expected.. 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
Advanced Component Testing Example
// components/UserProfile.js
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Alert,
} from 'react-native';
const UserProfile = ({ userId, onSave, onCancel }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
});
const [errors, setErrors] = useState({});
useEffect(() => {
loadUser();
}, [userId]);
const loadUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
setFormData({
name: userData.name || '',
email: userData.email || '',
phone: userData.phone || '',
});
} catch (error) {
Alert.alert('Error', 'Failed to load user data');
} finally {
setLoading(false);
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}
if (!formData.phone.trim()) {
newErrors.phone = 'Phone is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSave = async () => {
if (!validateForm()) return;
try {
setSaving(true);
await onSave(userId, formData);
Alert.alert('Success', 'Profile updated successfully');
} catch (error) {
Alert.alert('Error', 'Failed to save profile');
} finally {
setSaving(false);
}
};
const handleInputChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: null }));
}
};
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#2196F3" testID="loading-indicator" />
<Text style={styles.loadingText}>Loading user profile...</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>User Profile</Text>
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Name</Text>
<TextInput
style={[styles.input, errors.name && styles.inputError]}
value={formData.name}
onChangeText={(value) => handleInputChange('name', value)}
placeholder="Enter your name"
testID="name-input"
/>
{errors.name && (
<Text style={styles.errorText} testID="name-error">
{errors.name}
</Text>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
style={[styles.input, errors.email && styles.inputError]}
value={formData.email}
onChangeText={(value) => handleInputChange('email', value)}
placeholder="Enter your email"
keyboardType="email-address"
autoCapitalize="none"
testID="email-input"
/>
{errors.email && (
<Text style={styles.errorText} testID="email-error">
{errors.email}
</Text>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Phone</Text>
<TextInput
style={[styles.input, errors.phone && styles.inputError]}
value={formData.phone}
onChangeText={(value) => handleInputChange('phone', value)}
placeholder="Enter your phone number"
keyboardType="phone-pad"
testID="phone-input"
/>
{errors.phone && (
<Text style={styles.errorText} testID="phone-error">
{errors.phone}
</Text>
)}
</View>
</View>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={onCancel}
testID="cancel-button"
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.saveButton, saving && styles.disabledButton]}
onPress={handleSave}
disabled={saving}
testID="save-button"
>
{saving ? (
<ActivityIndicator size="small" color="white" testID="saving-indicator" />
) : (
<Text style={styles.saveButtonText}>Save</Text>
)}
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 30,
color: '#333',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: '#666',
},
form: {
backgroundColor: 'white',
borderRadius: 8,
padding: 20,
marginBottom: 20,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 16,
fontWeight: '500',
color: '#333',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: 'white',
},
inputError: {
borderColor: '#f44336',
},
errorText: {
color: '#f44336',
fontSize: 14,
marginTop: 5,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
},
button: {
flex: 1,
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
marginHorizontal: 5,
},
cancelButton: {
backgroundColor: '#6c757d',
},
saveButton: {
backgroundColor: '#2196F3',
},
disabledButton: {
backgroundColor: '#ccc',
},
cancelButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
saveButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
export default UserProfile;
// __tests__/components/UserProfile.test.js
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { Alert } from 'react-native';
import UserProfile from '../components/UserProfile';
// Mock fetch
global.fetch = jest.fn();
// Mock Alert
jest.spyOn(Alert, 'alert');
describe('UserProfile Component', () => {
const mockOnSave = jest.fn();
const mockOnCancel = jest.fn();
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
phone: '123-456-7890',
};
beforeEach(() => {
jest.clearAllMocks();
fetch.mockClear();
Alert.alert.mockClear();
});
it('should render loading state initially', () => {
fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
const { getByText, getByTestId } = render(
<UserProfile userId={1} onSave={mockOnSave} onCancel={mockOnCancel} />
);
expect(getByText('Loading user profile...')).toBeTruthy();
expect(getByTestId('loading-indicator')).toBeTruthy();
});
it('should load and display user data', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
const { getByDisplayValue } = render(
<UserProfile userId={1} onSave={mockOnSave} onCancel={mockOnCancel} />
);
await waitFor(() => {
expect(getByDisplayValue('John Doe')).toBeTruthy();
expect(getByDisplayValue('john@example.com')).toBeTruthy();
expect(getByDisplayValue('123-456-7890')).toBeTruthy();
});
});
it('should handle form validation', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
const { getByTestId, getByText } = render(
<UserProfile userId={1} onSave={mockOnSave} onCancel={mockOnCancel} />
);
await waitFor(() => {
expect(getByTestId('name-input')).toBeTruthy();
});
// Clear name field to trigger validation
fireEvent.changeText(getByTestId('name-input'), '');
fireEvent.press(getByTestId('save-button'));
await waitFor(() => {
expect(getByTestId('name-error')).toBeTruthy();
expect(getByText('Name is required')).toBeTruthy();
});
});
it('should call onSave with correct data when form is valid', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
mockOnSave.mockResolvedValueOnce();
const { getByTestId } = render(
<UserProfile userId={1} onSave={mockOnSave} onCancel={mockOnCancel} />
);
await waitFor(() => {
expect(getByTestId('save-button')).toBeTruthy();
});
fireEvent.press(getByTestId('save-button'));
await waitFor(() => {
expect(mockOnSave).toHaveBeenCalledWith(1, {
name: 'John Doe',
email: 'john@example.com',
phone: '123-456-7890',
});
});
});
});