device-testing
SKILL.md
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
Weekly Installs
10
Repository
anton-abyzov/specweaveInstalled on
claude-code9
opencode7
cursor7
codex7
antigravity7
gemini-cli7