name: 测试模式 description: Jest测试模式、工厂函数、模拟策略和TDD工作流。用于编写单元测试、创建测试工厂或遵循TDD红绿重构周期。
测试模式与工具
测试理念
测试驱动开发 (TDD):
- 首先编写失败的测试
- 实现最小代码以通过测试
- 在绿色后重构
- 从不编写生产代码而没有失败的测试
行为驱动测试:
- 测试行为,而不是实现
- 关注公共API和业务需求
- 避免测试实现细节
- 使用描述行为的描述性测试名称
工厂模式:
- 创建
getMockX(overrides?: Partial<X>)函数 - 提供合理的默认值
- 允许覆盖特定属性
- 保持测试DRY和可维护
测试工具
自定义渲染函数
创建一个自定义渲染,包装组件与所需的提供者:
// src/utils/testUtils.tsx
import { render } from '@testing-library/react-native';
import { ThemeProvider } from './theme';
export const renderWithTheme = (ui: React.ReactElement) => {
return render(
<ThemeProvider>{ui}</ThemeProvider>
);
};
用法:
import { renderWithTheme } from 'utils/testUtils';
import { screen } from '@testing-library/react-native';
it('应该渲染组件', () => {
renderWithTheme(<MyComponent />);
expect(screen.getByText('Hello')).toBeTruthy();
});
工厂模式
组件属性工厂
import { ComponentProps } from 'react';
const getMockMyComponentProps = (
overrides?: Partial<ComponentProps<typeof MyComponent>>
) => {
return {
title: '默认标题',
count: 0,
onPress: jest.fn(),
isLoading: false,
...overrides,
};
};
// 在测试中的用法
it('应该渲染自定义标题', () => {
const props = getMockMyComponentProps({ title: '自定义标题' });
renderWithTheme(<MyComponent {...props} />);
expect(screen.getByText('自定义标题')).toBeTruthy();
});
数据工厂
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
const getMockUser = (overrides?: Partial<User>): User => {
return {
id: '123',
name: 'John Doe',
email: 'john@example.com',
role: 'user',
...overrides,
};
};
// 用法
it('应该为管理员用户显示管理员徽章', () => {
const user = getMockUser({ role: 'admin' });
renderWithTheme(<UserCard user={user} />);
expect(screen.getByText('Admin')).toBeTruthy();
});
模拟模式
模拟模块
// 模拟整个模块
jest.mock('utils/analytics');
// 使用工厂函数模拟
jest.mock('utils/analytics', () => ({
Analytics: {
logEvent: jest.fn(),
},
}));
// 在测试中访问模拟
const mockLogEvent = jest.requireMock('utils/analytics').Analytics.logEvent;
模拟GraphQL钩子
jest.mock('./GetItems.generated', () => ({
useGetItemsQuery: jest.fn(),
}));
const mockUseGetItemsQuery = jest.requireMock(
'./GetItems.generated'
).useGetItemsQuery as jest.Mock;
// 在测试中
mockUseGetItemsQuery.mockReturnValue({
data: { items: [] },
loading: false,
error: undefined,
});
测试结构
describe('组件名称', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('渲染', () => {
it('应该用默认属性渲染组件', () => {});
it('应该在加载时渲染加载状态', () => {});
});
describe('用户交互', () => {
it('点击按钮时应调用onPress', async () => {});
});
describe('边缘情况', () => {
it('应该优雅地处理空数据', () => {});
});
});
查询模式
// 元素必须存在
expect(screen.getByText('Hello')).toBeTruthy();
// 元素不应存在
expect(screen.queryByText('Goodbye')).toBeNull();
// 元素异步出现
await waitFor(() => {
expect(screen.findByText('Loaded')).toBeTruthy();
});
用户交互模式
import { fireEvent, screen } from '@testing-library/react-native';
it('点击按钮时应提交表单', async () => {
const onSubmit = jest.fn();
renderWithTheme(<LoginForm onSubmit={onSubmit} />);
fireEvent.changeText(screen.getByLabelText('Email'), 'user@example.com');
fireEvent.changeText(screen.getByLabelText('Password'), 'password123');
fireEvent.press(screen.getByTestId('login-button'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalled();
});
});
要避免的反模式
测试模拟行为而不是实际行为
// 坏 - 测试模拟
expect(mockFetchData).toHaveBeenCalled();
// 好 - 测试实际行为
expect(screen.getByText('John Doe')).toBeTruthy();
不使用工厂函数
// 坏 - 重复、不一致的测试数据
it('测试1', () => {
const user = { id: '1', name: 'John', email: 'john@test.com', role: 'user' };
});
it('测试2', () => {
const user = { id: '2', name: 'Jane', email: 'jane@test.com' }; // 缺少角色!
});
// 好 - 可重用的工厂
const user = getMockUser({ name: '自定义名称' });
最佳实践
- 始终使用工厂函数 用于属性和数据
- 测试行为,而不是实现
- 使用描述性测试名称
- 使用描述块组织
- 在测试间清除模拟
- 保持测试专注 - 每个测试一个行为
运行测试
# 运行所有测试
npm test
# 带覆盖率运行
npm run test:coverage
# 运行特定文件
npm test ComponentName.test.tsx
与其他技能集成
- react-ui-patterns: 测试所有UI状态(加载、错误、空、成功)
- systematic-debugging: 修复前编写重现错误的测试