device-testing
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
// UserProfile.test.js
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
// useCounter.test.js
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
// api.test.js
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
// Button.test.js
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
// __mocks__/react-native-camera.js
export const RNCamera = {
Constants: {
Type: {
back: 'back',
front: 'front'
}
}
};
// In test file
jest.mock('react-native-camera', () => require('./__mocks__/react-native-camera'));
// Or inline mock
jest.mock('react-native-camera', () => ({
RNCamera: {
Constants: {
Type: { back: 'back', front: 'front' }
}
}
}));
Mocking AsyncStorage
// Setup file (jest.setup.js)
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
// In test
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
// Mock React Navigation
jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({
navigate: jest.fn(),
goBack: jest.fn()
})
}));
// In test
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
// Using jest.mock
jest.mock('./api', () => ({
fetchUser: jest.fn(() => Promise.resolve({
id: '123',
name: 'Mock User'
}))
}));
// Using MSW (Mock Service Worker)
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';
// By text
screen.getByText('Submit');
screen.findByText('Loading...'); // Async
screen.queryByText('Error'); // Returns null if not found
// By testID
<View testID="profile-container" />
screen.getByTestId('profile-container');
// By placeholder
<TextInput placeholder="Enter email" />
screen.getByPlaceholderText('Enter email');
// By display value
screen.getByDisplayValue('john@example.com');
// Multiple queries
screen.getAllByText('Item'); // Returns array
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} />
);
// Type into inputs
fireEvent.changeText(getByPlaceholderText('Email'), 'test@example.com');
fireEvent.changeText(getByPlaceholderText('Password'), 'password123');
// Press button
fireEvent.press(getByText('Login'));
// Wait for async operation
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
});
});
E2E Testing with Detox
Installation
# Install Detox
npm install --save-dev detox
# iOS: Install dependencies
brew tap wix/brew
brew install applesimutils
# Initialize Detox
detox init
# Build app for testing (iOS)
detox build --configuration ios.sim.debug
# Run tests
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
// e2e/login.test.js
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should login successfully with valid credentials', async () => {
// Type email
await element(by.id('email-input')).typeText('test@example.com');
// Type password
await element(by.id('password-input')).typeText('password123');
// Tap login button
await element(by.id('login-button')).tap();
// Verify navigation to home screen
await expect(element(by.id('home-screen'))).toBeVisible();
});
it('should show error with invalid credentials', async () => {
await element(by.id('email-input')).typeText('invalid@example.com');
await element(by.id('password-input')).typeText('wrong');
await element(by.id('login-button')).tap();
await expect(element(by.text('Invalid credentials'))).toBeVisible();
});
it('should scroll to bottom of list', async () => {
await element(by.id('user-list')).scrollTo('bottom');
await expect(element(by.id('load-more-button'))).toBeVisible();
});
});
Advanced Detox Actions
// Swipe
await element(by.id('carousel')).swipe('left', 'fast', 0.75);
// Scroll
await element(by.id('scroll-view')).scroll(200, 'down');
// Long press
await element(by.id('item-1')).longPress();
// Multi-tap
await element(by.id('like-button')).multiTap(2);
// Wait for element
await waitFor(element(by.id('success-message')))
.toBeVisible()
.withTimeout(5000);
// Take screenshot
await device.takeScreenshot('login-success');
Maestro (Alternative E2E Tool)
Installation
# Install Maestro
curl -Ls "https://get.maestro.mobile.dev" | bash
# Verify installation
maestro --version
Maestro Flow (YAML-based)
# flows/login.yaml
appId: com.myapp
---
# Launch app
- launchApp
# Wait for login screen
- assertVisible: "Login"
# Enter credentials
- tapOn: "Email"
- inputText: "test@example.com"
- tapOn: "Password"
- inputText: "password123"
# Submit
- tapOn: "Login"
# Verify success
- assertVisible: "Welcome"
Run Maestro Flow
# iOS Simulator
maestro test flows/login.yaml
# Android Emulator
maestro test --platform android flows/login.yaml
# Real device (USB connected)
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';
// Mock native modules
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
// Mock AsyncStorage
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
// Global test utilities
global.fetch = jest.fn();
// Silence console warnings in tests
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:
// In component
<TouchableOpacity testID="submit-button" onPress={handleSubmit}>
<Text>Submit</Text>
</TouchableOpacity>
// In Detox test
await element(by.id('submit-button')).tap();
// Avoid using text or accessibility labels (can change with i18n)
2. Test Factories for Mock Data
// testUtils/factories.js
export const createMockUser = (overrides = {}) => ({
id: '123',
name: 'John Doe',
email: 'john@example.com',
...overrides
});
// In test
const user = createMockUser({ name: 'Jane Doe' });
3. Custom Render with Providers
// testUtils/render.js
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
);
}
// In test
import { renderWithProviders } from './testUtils/render';
renderWithProviders(<MyScreen />);
4. Parallel Test Execution
// package.json
{
"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
More from anton-abyzov/specweave
technical-writing
Technical writing expert for API documentation, README files, tutorials, changelog management, and developer documentation. Covers style guides, information architecture, versioning docs, OpenAPI/Swagger, and documentation-as-code. Activates for technical writing, API docs, README, changelog, tutorial writing, documentation, technical communication, style guide, OpenAPI, Swagger, developer docs.
45spec-driven-brainstorming
Spec-driven brainstorming and product discovery expert. Helps teams ideate features, break down epics, conduct story mapping sessions, prioritize using MoSCoW/RICE/Kano, and validate ideas with lean startup methods. Activates for brainstorming, product discovery, story mapping, feature ideation, prioritization, MoSCoW, RICE, Kano model, lean startup, MVP definition, product backlog, feature breakdown.
43kafka-architecture
Apache Kafka architecture expert for cluster design, capacity planning, and high availability. Use when designing Kafka clusters, choosing partition strategies, or sizing brokers for production workloads.
34docusaurus
Docusaurus 3.x documentation framework - MDX authoring, theming, versioning, i18n. Use for documentation sites or spec-weave.com.
29frontend
Expert frontend developer for React, Vue, Angular, and modern JavaScript/TypeScript. Use when creating components, implementing hooks, handling state management, or building responsive web interfaces. Covers React 18+ features, custom hooks, form handling, and accessibility best practices.
29reflect
Self-improving AI memory system that persists learnings across sessions in CLAUDE.md. Use when capturing corrections, remembering user preferences, or extracting patterns from successful implementations. Enables continual learning without starting from zero each conversation.
27