| name | device-testing |
| description | Expert in React Native testing strategies including unit tests with Jest, integration tests, E2E tests with Detox, component testing with React Native Testing Library, snapshot testing, mocking native modules, testing on simulators and real devices. Activates for testing, jest, detox, e2e, unit test, integration test, component test, test runner, mock, snapshot test, testing library, react native testing library, test automation. |
Device Testing Expert
Comprehensive expertise in React Native testing strategies, from unit tests to end-to-end testing on real devices and simulators. Specializes in Jest, Detox, React Native Testing Library, and mobile testing best practices.
What I Know
Testing Pyramid for Mobile
Three Layers
- Unit Tests (70%): Fast, isolated, test logic
- Integration Tests (20%): Test component integration
- E2E Tests (10%): Test full user flows on devices
Tools
- Jest: Unit and integration testing
- React Native Testing Library: Component testing
- Detox: E2E testing on simulators/emulators
- Maestro: Alternative E2E testing (newer)
Unit Testing with Jest
Basic Component Test
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import UserProfile from './UserProfile';
describe('UserProfile', () => {
it('renders user name correctly', () => {
const user = { name: 'John Doe', email: 'john@example.com' };
const { getByText } = render(<UserProfile user={user} />);
expect(getByText('John Doe')).toBeTruthy();
expect(getByText('john@example.com')).toBeTruthy();
});
it('calls onPress when button is pressed', () => {
const onPress = jest.fn();
const { getByText } = render(
<UserProfile user={{ name: 'John' }} onPress={onPress} />
);
fireEvent.press(getByText('Edit Profile'));
expect(onPress).toHaveBeenCalledTimes(1);
});
});
Testing Hooks
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('decrements counter', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
});
Async Testing
import { fetchUser } from './api';
describe('fetchUser', () => {
it('fetches user data successfully', async () => {
const user = await fetchUser('123');
expect(user).toEqual({
id: '123',
name: 'John Doe',
email: 'john@example.com'
});
});
it('handles errors gracefully', async () => {
await expect(fetchUser('invalid')).rejects.toThrow('User not found');
});
});
Snapshot Testing
import React from 'react';
import { render } from '@testing-library/react-native';
import Button from './Button';
describe('Button', () => {
it('renders correctly', () => {
const { toJSON } = render(<Button title="Press Me" />);
expect(toJSON()).toMatchSnapshot();
});
it('renders with custom color', () => {
const { toJSON } = render(<Button title="Press Me" color="red" />);
expect(toJSON()).toMatchSnapshot();
});
});
Mocking
Mocking Native Modules
export const RNCamera = {
Constants: {
Type: {
back: 'back',
front: 'front'
}
}
};
jest.mock('react-native-camera', () => require('./__mocks__/react-native-camera'));
jest.mock('react-native-camera', () => ({
RNCamera: {
Constants: {
Type: { back: 'back', front: 'front' }
}
}
}));
Mocking AsyncStorage
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
import AsyncStorage from '@react-native-async-storage/async-storage';
describe('Storage', () => {
beforeEach(() => {
AsyncStorage.clear();
});
it('stores and retrieves data', async () => {
await AsyncStorage.setItem('key', 'value');
const value = await AsyncStorage.getItem('key');
expect(value).toBe('value');
});
});
Mocking Navigation
jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({
navigate: jest.fn(),
goBack: jest.fn()
})
}));
import { useNavigation } from '@react-navigation/native';
describe('ProfileScreen', () => {
it('navigates to settings on button press', () => {
const navigate = jest.fn();
useNavigation.mockReturnValue({ navigate });
const { getByText } = render(<ProfileScreen />);
fireEvent.press(getByText('Settings'));
expect(navigate).toHaveBeenCalledWith('Settings');
});
});
Mocking API Calls
jest.mock('./api', () => ({
fetchUser: jest.fn(() => Promise.resolve({
id: '123',
name: 'Mock User'
}))
}));
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/user/:id', (req, res, ctx) => {
return res(ctx.json({
id: req.params.id,
name: 'Mock User'
}));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Component Testing with React Native Testing Library
Queries
import { render, screen } from '@testing-library/react-native';
screen.getByText('Submit');
screen.findByText('Loading...');
screen.queryByText('Error');
<View testID="profile-container" />
screen.getByTestId('profile-container');
<TextInput placeholder="Enter email" />
screen.getByPlaceholderText('Enter email');
screen.getByDisplayValue('john@example.com');
screen.getAllByText('Item');
User Interactions
import { render, fireEvent, waitFor } from '@testing-library/react-native';
describe('LoginForm', () => {
it('submits form with valid data', async () => {
const onSubmit = jest.fn();
const { getByPlaceholderText, getByText } = render(
<LoginForm onSubmit={onSubmit} />
);
fireEvent.changeText(getByPlaceholderText('Email'), 'test@example.com');
fireEvent.changeText(getByPlaceholderText('Password'), 'password123');
fireEvent.press(getByText('Login'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
});
});
E2E Testing with Detox
Installation
npm install --save-dev detox
brew tap wix/brew
brew install applesimutils
detox init
detox build --configuration ios.sim.debug
detox test --configuration ios.sim.debug
Configuration (.detoxrc.js)
module.exports = {
testRunner: 'jest',
runnerConfig: 'e2e/config.json',
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app',
build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug'
}
},
devices: {
simulator: {
type: 'ios.simulator',
device: { type: 'iPhone 15 Pro' }
},
emulator: {
type: 'android.emulator',
device: { avdName: 'Pixel_6_API_34' }
}
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug'
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug'
}
}
};
Writing Detox Tests
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should login successfully with valid credentials', async () => {
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await expect(element(by.id('home-screen'))).toBeVisible();
});
it('should show error with invalid credentials', async () => {
await element(by.()).();
(by.()).();
(by.()).();
((by.())).();
});
(, () => {
(by.()).();
((by.())).();
});
});
Advanced Detox Actions
await element(by.id('carousel')).swipe('left', 'fast', 0.75);
await element(by.id('scroll-view')).scroll(200, 'down');
await element(by.id('item-1')).longPress();
await element(by.id('like-button')).multiTap(2);
await waitFor(element(by.id('success-message')))
.toBeVisible()
.withTimeout(5000);
await device.takeScreenshot('login-success');
Maestro (Alternative E2E Tool)
Installation
curl -Ls "https://get.maestro.mobile.dev" | bash
maestro --version
Maestro Flow (YAML-based)
appId: com.myapp
---
- launchApp
- assertVisible: "Login"
- tapOn: "Email"
- inputText: "test@example.com"
- tapOn: "Password"
- inputText: "password123"
- tapOn: "Login"
- assertVisible: "Welcome"
Run Maestro Flow
maestro test flows/login.yaml
maestro test --platform android flows/login.yaml
maestro test --device <device-id> flows/login.yaml
When to Use This Skill
Ask me when you need help with:
- Setting up Jest for React Native
- Writing unit tests for components and hooks
- Mocking native modules and dependencies
- Writing integration tests
- Setting up Detox or Maestro for E2E testing
- Testing asynchronous operations
- Snapshot testing strategies
- Testing navigation flows
- Debugging test failures
- Running tests in CI/CD pipelines
- Testing on real devices
- Performance testing strategies
Test Configuration
Jest Configuration (jest.config.js)
module.exports = {
preset: 'react-native',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|@react-navigation|expo|@expo)/)'
],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}',
'!src/**/__tests__/**'
],
coverageThreshold: {
global: {
statements: 80,
branches: 75,
functions: 80,
lines: 80
}
}
};
Jest Setup (jest.setup.js)
import 'react-native-gesture-handler/jestSetup';
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
global.fetch = jest.fn();
global.console = {
...console,
warn: jest.fn(),
error: jest.fn()
};
Pro Tips & Tricks
1. Test IDs for E2E Testing
Add testID to components for reliable selectors:
<TouchableOpacity testID="submit-button" onPress={handleSubmit}>
<Text>Submit</Text>
</TouchableOpacity>
await element(by.id('submit-button')).tap();
2. Test Factories for Mock Data
export const createMockUser = (overrides = {}) => ({
id: '123',
name: 'John Doe',
email: 'john@example.com',
...overrides
});
const user = createMockUser({ name: 'Jane Doe' });
3. Custom Render with Providers
import { render } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { Provider } from 'react-redux';
import { store } from '../store';
export function renderWithProviders(ui, options = {}) {
return render(
<Provider store={store}>
<NavigationContainer>
{ui}
</NavigationContainer>
</Provider>,
options
);
}
import { renderWithProviders } from './testUtils/render';
renderWithProviders(<MyScreen />);
4. Parallel Test Execution
{
"scripts": {
"test": "jest --maxWorkers=4",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Integration with SpecWeave
Test Planning
- Document test strategy in
spec.md
- Include test coverage targets in
tasks.md
- Embed test cases in tasks (BDD format)
Coverage Tracking
- Set coverage thresholds (80%+ for critical paths)
- Track coverage trends across increments
- Document testing gaps in increment reports
CI/CD Integration
- Run tests on every commit
- Block merges if tests fail
- Generate coverage reports
- Run E2E tests on staging builds