react-native-web-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Native Web - Testing
React Native Web - 测试
Comprehensive testing patterns for React Native Web applications using Jest and React Native Testing Library.
使用Jest和React Native Testing Library进行React Native Web应用程序测试的全面模式。
Key Concepts
核心概念
React Native Testing Library
React Native Testing Library
The standard testing library for React Native components:
typescript
import { render, screen, fireEvent } from '@testing-library/react-native';
import { Button } from './Button';
describe('Button', () => {
it('calls onPress when pressed', () => {
const onPress = jest.fn();
render(<Button title="Click me" onPress={onPress} />);
const button = screen.getByText('Click me');
fireEvent.press(button);
expect(onPress).toHaveBeenCalledTimes(1);
});
});React Native组件的标准测试库:
typescript
import { render, screen, fireEvent } from '@testing-library/react-native';
import { Button } from './Button';
describe('Button', () => {
it('calls onPress when pressed', () => {
const onPress = jest.fn();
render(<Button title="Click me" onPress={onPress} />);
const button = screen.getByText('Click me');
fireEvent.press(button);
expect(onPress).toHaveBeenCalledTimes(1);
});
});Jest Configuration
Jest配置
Configure Jest for React Native Web:
javascript
// jest.config.js
module.exports = {
preset: 'react-native',
moduleNameMapper: {
'^react-native$': 'react-native-web',
},
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|react-native-web)/)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};为React Native Web配置Jest:
javascript
// jest.config.js
module.exports = {
preset: 'react-native',
moduleNameMapper: {
'^react-native$': 'react-native-web',
},
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|react-native-web)/)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};Testing Utilities
测试工具
Common testing utilities and helpers:
typescript
import { render, RenderOptions } from '@testing-library/react-native';
import { ReactElement } from 'react';
import { ThemeProvider } from './theme';
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
theme?: Theme;
}
export function renderWithProviders(
ui: ReactElement,
{ theme = defaultTheme, ...options }: CustomRenderOptions = {}
) {
return render(
<ThemeProvider value={theme}>
{ui}
</ThemeProvider>,
options
);
}常用测试工具与辅助函数:
typescript
import { render, RenderOptions } from '@testing-library/react-native';
import { ReactElement } from 'react';
import { ThemeProvider } from './theme';
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
theme?: Theme;
}
export function renderWithProviders(
ui: ReactElement,
{ theme = defaultTheme, ...options }: CustomRenderOptions = {}
) {
return render(
<ThemeProvider value={theme}>
{ui}
</ThemeProvider>,
options
);
}Best Practices
最佳实践
Component Testing
组件测试
✅ Test user interactions and behavior:
typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits form with valid credentials', async () => {
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
const emailInput = screen.getByPlaceholderText('Email');
const passwordInput = screen.getByPlaceholderText('Password');
const submitButton = screen.getByText('Login');
fireEvent.changeText(emailInput, 'user@example.com');
fireEvent.changeText(passwordInput, 'password123');
fireEvent.press(submitButton);
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
});
});
it('shows error for invalid email', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
const emailInput = screen.getByPlaceholderText('Email');
const submitButton = screen.getByText('Login');
fireEvent.changeText(emailInput, 'invalid-email');
fireEvent.press(submitButton);
await waitFor(() => {
expect(screen.getByText('Invalid email address')).toBeTruthy();
});
});
});✅ 测试用户交互与行为:
typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits form with valid credentials', async () => {
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
const emailInput = screen.getByPlaceholderText('Email');
const passwordInput = screen.getByPlaceholderText('Password');
const submitButton = screen.getByText('Login');
fireEvent.changeText(emailInput, 'user@example.com');
fireEvent.changeText(passwordInput, 'password123');
fireEvent.press(submitButton);
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
});
});
it('shows error for invalid email', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
const emailInput = screen.getByPlaceholderText('Email');
const submitButton = screen.getByText('Login');
fireEvent.changeText(emailInput, 'invalid-email');
fireEvent.press(submitButton);
await waitFor(() => {
expect(screen.getByText('Invalid email address')).toBeTruthy();
});
});
});Async Testing
异步测试
✅ Use waitFor for async operations:
typescript
import { render, screen, waitFor } from '@testing-library/react-native';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('loads and displays user data', async () => {
const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' };
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockUser),
})
) as jest.Mock;
render(<UserProfile userId="1" />);
// Check loading state
expect(screen.getByTestId('loading-indicator')).toBeTruthy();
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeTruthy();
expect(screen.getByText('john@example.com')).toBeTruthy();
});
expect(screen.queryByTestId('loading-indicator')).toBeNull();
});
});✅ 使用waitFor处理异步操作:
typescript
import { render, screen, waitFor } from '@testing-library/react-native';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('loads and displays user data', async () => {
const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' };
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockUser),
})
) as jest.Mock;
render(<UserProfile userId="1" />);
// 检查加载状态
expect(screen.getByTestId('loading-indicator')).toBeTruthy();
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeTruthy();
expect(screen.getByText('john@example.com')).toBeTruthy();
});
expect(screen.queryByTestId('loading-indicator')).toBeNull();
});
});Mocking Modules
模块模拟
✅ Mock navigation and other dependencies:
typescript
import { render, screen, fireEvent } from '@testing-library/react-native';
// Mock navigation
const mockNavigate = jest.fn();
jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({
navigate: mockNavigate,
}),
}));
describe('HomeScreen', () => {
it('navigates to details on item press', () => {
render(<HomeScreen />);
const item = screen.getByText('Item 1');
fireEvent.press(item);
expect(mockNavigate).toHaveBeenCalledWith('Details', { id: '1' });
});
});✅ 模拟导航及其他依赖项:
typescript
import { render, screen, fireEvent } from '@testing-library/react-native';
// Mock navigation
const mockNavigate = jest.fn();
jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({
navigate: mockNavigate,
}),
}));
describe('HomeScreen', () => {
it('navigates to details on item press', () => {
render(<HomeScreen />);
const item = screen.getByText('Item 1');
fireEvent.press(item);
expect(mockNavigate).toHaveBeenCalledWith('Details', { id: '1' });
});
});Examples
示例
Testing Custom Hooks
自定义Hook测试
typescript
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
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);
});
});typescript
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
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);
});
});Testing with Context
Context测试
typescript
import { render, screen } from '@testing-library/react-native';
import { AuthProvider } from './auth-context';
import { ProtectedScreen } from './ProtectedScreen';
describe('ProtectedScreen', () => {
it('shows content when authenticated', () => {
const mockUser = { id: '1', name: 'John' };
render(
<AuthProvider initialUser={mockUser}>
<ProtectedScreen />
</AuthProvider>
);
expect(screen.getByText('Welcome, John')).toBeTruthy();
});
it('shows login prompt when not authenticated', () => {
render(
<AuthProvider initialUser={null}>
<ProtectedScreen />
</AuthProvider>
);
expect(screen.getByText('Please log in')).toBeTruthy();
});
});typescript
import { render, screen } from '@testing-library/react-native';
import { AuthProvider } from './auth-context';
import { ProtectedScreen } from './ProtectedScreen';
describe('ProtectedScreen', () => {
it('shows content when authenticated', () => {
const mockUser = { id: '1', name: 'John' };
render(
<AuthProvider initialUser={mockUser}>
<ProtectedScreen />
</AuthProvider>
);
expect(screen.getByText('Welcome, John')).toBeTruthy();
});
it('shows login prompt when not authenticated', () => {
render(
<AuthProvider initialUser={null}>
<ProtectedScreen />
</AuthProvider>
);
expect(screen.getByText('Please log in')).toBeTruthy();
});
});Snapshot Testing
快照测试
typescript
import { render } from '@testing-library/react-native';
import { Card } from './Card';
describe('Card', () => {
it('matches snapshot', () => {
const { toJSON } = render(
<Card title="Test Card" description="Test description" />
);
expect(toJSON()).toMatchSnapshot();
});
});typescript
import { render } from '@testing-library/react-native';
import { Card } from './Card';
describe('Card', () => {
it('matches snapshot', () => {
const { toJSON } = render(
<Card title="Test Card" description="Test description" />
);
expect(toJSON()).toMatchSnapshot();
});
});Integration Testing
集成测试
typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { HomeScreen } from './HomeScreen';
import { DetailsScreen } from './DetailsScreen';
const Stack = createNativeStackNavigator();
function TestApp() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
describe('Navigation Flow', () => {
it('navigates from home to details', async () => {
render(<TestApp />);
// On Home screen
expect(screen.getByText('Home Screen')).toBeTruthy();
// Navigate to Details
const item = screen.getByText('View Details');
fireEvent.press(item);
// Wait for Details screen
await waitFor(() => {
expect(screen.getByText('Details Screen')).toBeTruthy();
});
});
});typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { HomeScreen } from './HomeScreen';
import { DetailsScreen } from './DetailsScreen';
const Stack = createNativeStackNavigator();
function TestApp() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
describe('Navigation Flow', () => {
it('navigates from home to details', async () => {
render(<TestApp />);
// On Home screen
expect(screen.getByText('Home Screen')).toBeTruthy();
// Navigate to Details
const item = screen.getByText('View Details');
fireEvent.press(item);
// Wait for Details screen
await waitFor(() => {
expect(screen.getByText('Details Screen')).toBeTruthy();
});
});
});Common Patterns
常见模式
Testing Forms
表单测试
typescript
describe('ContactForm', () => {
it('validates all fields before submit', async () => {
const onSubmit = jest.fn();
render(<ContactForm onSubmit={onSubmit} />);
const submitButton = screen.getByText('Submit');
fireEvent.press(submitButton);
await waitFor(() => {
expect(screen.getByText('Name is required')).toBeTruthy();
expect(screen.getByText('Email is required')).toBeTruthy();
expect(onSubmit).not.toHaveBeenCalled();
});
});
});typescript
describe('ContactForm', () => {
it('validates all fields before submit', async () => {
const onSubmit = jest.fn();
render(<ContactForm onSubmit={onSubmit} />);
const submitButton = screen.getByText('Submit');
fireEvent.press(submitButton);
await waitFor(() => {
expect(screen.getByText('Name is required')).toBeTruthy();
expect(screen.getByText('Email is required')).toBeTruthy();
expect(onSubmit).not.toHaveBeenCalled();
});
});
});Testing Lists
列表测试
typescript
describe('ItemsList', () => {
it('renders all items', () => {
const items = [
{ id: '1', title: 'Item 1' },
{ id: '2', title: 'Item 2' },
{ id: '3', title: 'Item 3' },
];
render(<ItemsList items={items} />);
items.forEach(item => {
expect(screen.getByText(item.title)).toBeTruthy();
});
});
it('handles empty state', () => {
render(<ItemsList items={[]} />);
expect(screen.getByText('No items found')).toBeTruthy();
});
});typescript
describe('ItemsList', () => {
it('renders all items', () => {
const items = [
{ id: '1', title: 'Item 1' },
{ id: '2', title: 'Item 2' },
{ id: '3', title: 'Item 3' },
];
render(<ItemsList items={items} />);
items.forEach(item => {
expect(screen.getByText(item.title)).toBeTruthy();
});
});
it('handles empty state', () => {
render(<ItemsList items={[]} />);
expect(screen.getByText('No items found')).toBeTruthy();
});
});Testing Accessibility
可访问性测试
typescript
describe('Button accessibility', () => {
it('has correct accessibility props', () => {
render(<Button title="Submit" onPress={jest.fn()} />);
const button = screen.getByRole('button');
expect(button).toHaveAccessibilityState({ disabled: false });
expect(button).toHaveAccessibilityHint('Submits the form');
});
it('is disabled when loading', () => {
render(<Button title="Submit" onPress={jest.fn()} loading />);
const button = screen.getByRole('button');
expect(button).toHaveAccessibilityState({ disabled: true, busy: true });
});
});typescript
describe('Button accessibility', () => {
it('has correct accessibility props', () => {
render(<Button title="Submit" onPress={jest.fn()} />);
const button = screen.getByRole('button');
expect(button).toHaveAccessibilityState({ disabled: false });
expect(button).toHaveAccessibilityHint('Submits the form');
});
it('is disabled when loading', () => {
render(<Button title="Submit" onPress={jest.fn()} loading />);
const button = screen.getByRole('button');
expect(button).toHaveAccessibilityState({ disabled: true, busy: true });
});
});Anti-Patterns
反模式
❌ Don't test implementation details:
typescript
// Bad - testing internal state
expect(component.state.count).toBe(5);
// Good - test observable behavior
expect(screen.getByText('Count: 5')).toBeTruthy();❌ Don't use querySelector or DOM methods:
typescript
// Bad
const element = container.querySelector('.button');
// Good
const button = screen.getByRole('button');❌ Don't create overly coupled tests:
typescript
// Bad - too specific
expect(screen.getByText('Submit')).toHaveStyle({
backgroundColor: '#007AFF',
paddingHorizontal: 16
});
// Good - test behavior
const button = screen.getByText('Submit');
expect(button).toBeTruthy();
fireEvent.press(button);
expect(mockSubmit).toHaveBeenCalled();❌ Don't forget to clean up:
typescript
// Bad
afterEach(() => {
// No cleanup
});
// Good
afterEach(() => {
jest.clearAllMocks();
cleanup();
});❌ 不要测试实现细节:
typescript
// Bad - testing internal state
expect(component.state.count).toBe(5);
// Good - test observable behavior
expect(screen.getByText('Count: 5')).toBeTruthy();❌ 不要使用querySelector或DOM方法:
typescript
// Bad
const element = container.querySelector('.button');
// Good
const button = screen.getByRole('button');❌ 不要创建过度耦合的测试:
typescript
// Bad - too specific
expect(screen.getByText('Submit')).toHaveStyle({
backgroundColor: '#007AFF',
paddingHorizontal: 16
});
// Good - test behavior
const button = screen.getByText('Submit');
expect(button).toBeTruthy();
fireEvent.press(button);
expect(mockSubmit).toHaveBeenCalled();❌ 不要忘记清理:
typescript
// Bad
afterEach(() => {
// No cleanup
});
// Good
afterEach(() => {
jest.clearAllMocks();
cleanup();
});Related Skills
相关技能
- react-native-web-core: Core React Native Web concepts
- react-native-web-navigation: Testing navigation flows
- react-native-web-performance: Performance testing
- react-native-web-core: React Native Web核心概念
- react-native-web-navigation: 测试导航流程
- react-native-web-performance: 性能测试