name: mobile-testing description: 使用Jest和React Native Testing Library为React Native应用编写和运行测试。适用于创建测试、调试失败或设置测试基础设施时使用。 allowed-tools: Bash, Read, Write, Edit
移动端测试
React Native应用程序测试指南。
何时使用
- 为组件或工具编写单元测试
- 为功能创建集成测试
- 设置测试基础设施
- 调试测试失败
- 提高测试覆盖率
测试设置
# 安装测试依赖
npm install --save-dev jest @testing-library/react-native
# 添加到package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
组件测试模板
import { render, fireEvent } from '@testing-library/react-native';
import { MyButton } from './MyButton';
describe('MyButton', () => {
it('正确渲染', () => {
const { getByText } = render(<MyButton title="点击我" />);
expect(getByText('点击我')).toBeTruthy();
});
it('按下时调用onPress', () => {
const onPress = jest.fn();
const { getByText } = render(
<MyButton title="点击我" onPress={onPress} />
);
fireEvent.press(getByText('点击我'));
expect(onPress).toHaveBeenCalledTimes(1);
});
});
Hook测试模板
import { renderHook, act } from '@testing-library/react-native';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('增加计数', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
工具函数测试模板
import { formatDate } from './formatDate';
describe('formatDate', () => {
it('正确格式化日期', () => {
const date = new Date('2025-01-15');
expect(formatDate(date)).toBe('2025-01-15');
});
it('处理无效输入', () => {
expect(() => formatDate(null)).toThrow();
});
});
模拟模式
模拟外部模块
jest.mock('expo-notifications', () => ({
scheduleNotificationAsync: jest.fn(),
}));
模拟数据库
jest.mock('@/db/client', () => ({
db: {
select: jest.fn(),
insert: jest.fn(),
},
}));
模拟导航
jest.mock('expo-router', () => ({
useRouter: () => ({
push: jest.fn(),
back: jest.fn(),
}),
}));
运行测试
# 运行所有测试
npm test
# 监听模式(变更时自动重新运行)
npm run test:watch
# 生成覆盖率报告
npm run test:coverage
# 运行特定测试文件
npm test -- MyComponent.test.tsx
# 更新快照
npm test -- -u
最佳实践
- 测试行为,而非实现:测试用户看到和操作的内容
- 使用
testID进行选择:比文本匹配更可靠 - 模拟外部依赖:保持测试隔离和快速
- 测试边界情况:空状态、错误、加载状态
- 保持测试简单:尽可能每个测试只有一个断言
- 清理:使用
beforeEach/afterEach进行设置/拆卸
常见模式
异步测试
it('加载数据', async () => {
const { findByText } = render(<DataComponent />);
expect(await findByText('已加载')).toBeTruthy();
});
测试表单
it('验证输入', () => {
const { getByTestId, getByText } = render(<LoginForm />);
fireEvent.changeText(getByTestId('email-input'), '无效');
fireEvent.press(getByTestId('submit-button'));
expect(getByText('邮箱无效')).toBeTruthy();
});
测试列表
it('渲染列表项', () => {
const items = [{ id: '1', name: '项目1' }];
const { getAllByTestId } = render(<ItemList items={items} />);
expect(getAllByTestId('list-item')).toHaveLength(1);
});
故障排除
- 测试超时:增加超时时间或检查未解决的Promise
- 找不到元素:使用
screen.debug()查看渲染输出 - 模拟不工作:确保模拟在导入之前
- 异步问题:使用
waitFor或findBy查询