The project uses comprehensive Jest configuration with:
jest.setup.js- Contains mocks for all native modulesjest.config.cjs- Configures transform patterns and module mappingtsconfig.test.json- Test-specific TypeScript configuration
moduleNameMapper: {
'^@env$': '<rootDir>/tests/__setup__/@env.js',
'\\.svg$': '<rootDir>/tests/__setup__/svgMock.js',
'^@/(.*)$': '<rootDir>/src/$1',
'^@tests/(.*)$': '<rootDir>/tests/src/$1',
}transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase)/)',
]All React Native native modules are mocked in jest.setup.js:
jest.mock('@react-native-firebase/messaging', () => {
return () => ({
hasPermission: jest.fn(() => Promise.resolve(true)),
requestPermission: jest.fn(() => Promise.resolve(true)),
getToken: jest.fn(() => Promise.resolve('mock-token')),
onMessage: jest.fn(() => jest.fn()),
onNotificationOpenedApp: jest.fn(() => jest.fn()),
getInitialNotification: jest.fn(() => Promise.resolve(null)),
setBackgroundMessageHandler: jest.fn(),
registerDeviceForRemoteMessages: jest.fn(() => Promise.resolve()),
subscribeToTopic: jest.fn(),
unsubscribeFromTopic: jest.fn(),
});
});jest.mock('react-native-keychain', () => ({
SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY',
SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE',
SECURITY_LEVEL_SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE',
setGenericPassword: jest.fn(),
getGenericPassword: jest.fn(),
resetGenericPassword: jest.fn(),
ACCESSIBLE: {
WHEN_UNLOCKED: 'AccessibleWhenUnlocked',
AFTER_FIRST_UNLOCK: 'AccessibleAfterFirstUnlock',
ALWAYS: 'AccessibleAlways',
WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: 'AccessibleWhenPasscodeSetThisDeviceOnly',
WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'AccessibleWhenUnlockedThisDeviceOnly',
AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: 'AccessibleAfterFirstUnlockThisDeviceOnly',
ALWAYS_THIS_DEVICE_ONLY: 'AccessibleAlwaysThisDeviceOnly',
},
ACCESS_CONTROL: {
USER_PRESENCE: 'UserPresence',
BIOMETRY_ANY: 'BiometryAny',
BIOMETRY_CURRENT_SET: 'BiometryCurrentSet',
DEVICE_PASSCODE: 'DevicePasscode',
APPLICATION_PASSWORD: 'ApplicationPassword',
BIOMETRY_ANY_OR_DEVICE_PASSCODE: 'BiometryAnyOrDevicePasscode',
BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE: 'BiometryCurrentSetOrDevicePasscode',
},
}));jest.mock('react-native-nfc-manager', () => ({
start: jest.fn(),
isSupported: jest.fn().mockResolvedValue(true),
isEnabled: jest.fn().mockResolvedValue(true),
registerTagEvent: jest.fn(),
unregisterTagEvent: jest.fn(),
requestTechnology: jest.fn(),
cancelTechnologyRequest: jest.fn(),
getTag: jest.fn(),
setAlertMessage: jest.fn(),
sendMifareCommand: jest.fn(),
sendCommandAPDU: jest.fn(),
transceive: jest.fn(),
getMaxTransceiveLength: jest.fn(),
setTimeout: jest.fn(),
connect: jest.fn(),
close: jest.fn(),
cleanUpTag: jest.fn(),
default: {
// Same methods as above
},
}));SQLite operations are mocked for testing:
// Mock react-native-sqlite-storage
jest.mock('react-native-sqlite-storage', () => ({
enablePromise: jest.fn(),
openDatabase: jest.fn(),
}));
// Test database instance
const mockDb = {
executeSql: jest.fn(() => Promise.resolve()),
};
mockSQLite.openDatabase.mockResolvedValue(mockDb);tests/
├── __setup__/ # Global test setup and mocks
├── src/ # Unit tests mirroring source structure
├── integration/ # Integration tests
├── e2e/ # End-to-end tests
└── utils/ # Test utilities and helpers
- Unit tests:
*.test.tsor*.test.tsx - Integration tests:
*.integration.test.ts - E2E tests: Platform-specific YAML files
Use renderHook for testing custom hooks:
import { renderHook } from '@testing-library/react-native';
describe('useModal', () => {
it('should return modal functions', () => {
const { result } = renderHook(() => useModal(mockParams));
expect(result.current).toHaveProperty('showModal');
expect(result.current).toHaveProperty('dismissModal');
expect(result.current).toHaveProperty('visible');
});
});Test React Native components with proper mocking:
import { render, fireEvent } from '@testing-library/react-native';
describe('Component', () => {
it('should handle user interactions', () => {
const mockNavigation = { navigate: jest.fn() };
const { getByText } = render(<Component navigation={mockNavigation} />);
fireEvent.press(getByText('Button'));
expect(mockNavigation.navigate).toHaveBeenCalledWith('Screen');
});
});Test error boundaries and error handling:
describe('Error handling', () => {
beforeEach(() => {
// Suppress console.error during tests
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should handle errors gracefully', () => {
// Test error scenarios
});
});- Separate test files for iOS and Android
- Maestro for cross-platform E2E testing
- Platform-specific build commands before E2E tests
# iOS E2E
yarn test:e2e:ios
# Android E2E
yarn test:e2e:android- Mock passport data for testing
- Test-specific environment variables
- Cleanup between test runs
# Analyze bundle size
yarn analyze:bundle:ios
yarn analyze:bundle:android
# Tree shaking analysis
yarn analyze:tree-shaking- Memory leak detection in tests
- Component lifecycle testing
- Native module cleanup verification
# Basic coverage
yarn test:coverage
# CI coverage with multiple formats
yarn test:coverage:ci- Jest coverage reporting with multiple formats
- CI-specific coverage commands
- Coverage thresholds for critical paths
- Always clean up mocks between tests
- Use
beforeEachandafterEachfor setup/cleanup - Avoid shared state between tests
- Mock at the right level (module vs function)
- Provide realistic mock return values
- Test error scenarios with mocks
- Use
async/awaitfor async operations - Test both success and failure paths
- Handle promises properly in tests
- Test platform-specific code paths
- Mock platform-specific modules
- Test conditional rendering logic