bulletproof-react测试策略:端到端测试与单元测试完整方案
🎯 为什么需要完整的测试策略?
在现代React应用开发中,测试不再是可选项而是必需品。一个健壮的测试策略能够:
- 提升代码质量:及早发现和修复bug
- 增强开发信心:重构时确保功能正常
- 提高交付速度:自动化测试减少手动回归
- 改善协作效率:测试用例作为活文档
📊 测试金字塔:构建分层测试体系
测试类型对比表
| 测试类型 | 覆盖范围 | 执行速度 | 维护成本 | 置信度 |
|---|---|---|---|---|
| 单元测试 | 单个组件/函数 | ⚡️ 极快 | 低 | 中等 |
| 集成测试 | 组件间交互 | 🏃 快速 | 中等 | 高 |
| 端到端测试 | 完整用户流程 | 🐢 慢 | 高 | 极高 |
🧪 单元测试:基础构建块
核心工具栈
// package.json 测试依赖
{
"devDependencies": {
"@testing-library/react": "^15.0.5",
"@testing-library/user-event": "^14.5.2",
"@testing-library/jest-dom": "^6.4.2",
"vitest": "^2.1.4",
"jsdom": "^24.0.0"
}
}
组件测试最佳实践
// 确认对话框组件测试示例
import { Button } from '@/components/ui/button';
import { rtlRender, screen, userEvent, waitFor } from '@/testing/test-utils';
import { ConfirmationDialog } from '../confirmation-dialog';
test('should handle confirmation flow', async () => {
const titleText = 'Are you sure?';
const bodyText = 'Are you sure you want to delete this item?';
const confirmationButtonText = 'Confirm';
const openButtonText = 'Open';
await rtlRender(
<ConfirmationDialog
icon="danger"
title={titleText}
body={bodyText}
confirmButton={<Button>{confirmationButtonText}</Button>}
triggerButton={<Button>{openButtonText}</Button>}
/>,
);
// 初始状态验证
expect(screen.queryByText(titleText)).not.toBeInTheDocument();
// 用户交互模拟
await userEvent.click(screen.getByRole('button', { name: openButtonText }));
// 对话框显示验证
expect(await screen.findByText(titleText)).toBeInTheDocument();
expect(screen.getByText(bodyText)).toBeInTheDocument();
// 取消操作验证
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
await waitFor(() =>
expect(screen.queryByText(titleText)).not.toBeInTheDocument(),
);
expect(screen.queryByText(bodyText)).not.toBeInTheDocument();
});
测试工具函数封装
// test-utils.tsx 核心工具函数
export const waitForLoadingToFinish = () =>
waitForElementToBeRemoved(
() => [
...screen.queryAllByTestId(/loading/i),
...screen.queryAllByText(/loading/i),
],
{ timeout: 4000 },
);
export const renderApp = async (
ui: any,
{ user, url = '/', path = '/', ...renderOptions }: Record<string, any> = {},
) => {
const initializedUser = await initializeUser(user);
const router = createMemoryRouter(
[{ path: path, element: ui }],
{ initialEntries: url ? ['/', url] : ['/'], initialIndex: url ? 1 : 0 }
);
const returnValue = {
...rtlRender(ui, {
wrapper: () => (
<AppProvider>
<RouterProvider router={router} />
</AppProvider>
),
...renderOptions,
}),
user: initializedUser,
};
await waitForLoadingToFinish();
return returnValue;
};
🌐 端到端测试:用户旅程验证
Playwright配置优化
// playwright.config.ts
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
trace: 'on-first-retry',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
testMatch: /.*\.spec\.ts/,
use: { ...devices['Desktop Chrome'], storageState: 'e2e/.auth/user.json' },
dependencies: ['setup'],
},
],
webServer: {
command: `yarn dev --port ${PORT}`,
timeout: 10 * 1000,
port: PORT,
reuseExistingServer: !process.env.CI,
},
});
完整的用户流程测试
// smoke.spec.ts - 完整的应用冒烟测试
import { test, expect } from '@playwright/test';
import { createDiscussion, createComment } from '../../src/testing/data-generators';
test('smoke', async ({ page }) => {
const discussion = createDiscussion();
const comment = createComment();
// 导航到首页
await page.goto('/');
await page.getByRole('button', { name: 'Get started' }).click();
await page.waitForURL('/app');
// 创建讨论
await page.getByRole('link', { name: 'Discussions' }).click();
await page.waitForURL('/app/discussions');
await page.getByRole('button', { name: 'Create Discussion' }).click();
await page.getByLabel('Title').click();
await page.getByLabel('Title').fill(discussion.title);
await page.getByLabel('Body').click();
await page.getByLabel('Body').fill(discussion.body);
await page.getByRole('button', { name: 'Submit' }).click();
// 验证讨论创建成功
await expect(page.getByRole('heading', { name: discussion.title })).toBeVisible();
await expect(page.getByText(discussion.body)).toBeVisible();
// 更新讨论
await page.getByRole('button', { name: 'Update Discussion' }).click();
await page.getByLabel('Title').fill(`${discussion.title} - updated`);
await page.getByLabel('Body').fill(`${discussion.body} - updated`);
await page.getByRole('button', { name: 'Submit' }).click();
// 创建和删除评论
await page.getByRole('button', { name: 'Create Comment' }).click();
await page.getByLabel('Body').fill(comment.body);
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText(comment.body)).toBeVisible();
// 删除评论
await page.getByRole('button', { name: 'Delete Comment' }).click();
await page.getByRole('button', { name: 'Delete Comment' }).click();
await expect(page.getByText(comment.body)).toBeHidden();
// 删除讨论
await page.getByRole('link', { name: 'Discussions' }).click();
await page.getByRole('button', { name: 'Delete Discussion' }).click();
await page.getByRole('button', { name: 'Delete Discussion' }).click();
await expect(page.getByRole('heading', { name: 'No Entries Found' })).toBeVisible();
});
🔧 Mock服务器:测试环境隔离
MSW配置和数据生成
// 数据生成器 - 创建测试数据
export const createUser = (userProperties?: any) => ({
id: faker.string.uuid(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
teamId: faker.helpers.arrayElement(['1', '2', '3']),
teamName: faker.helpers.arrayElement(['Design', 'Development', 'Marketing']),
role: faker.helpers.arrayElement(['USER', 'ADMIN']),
bio: faker.person.bio(),
createdAt: faker.date.past(),
...userProperties,
});
// API处理器 - 模拟后端响应
export const handlers = [
rest.post(`${config.apiUrl}/auth/login`, async (req, res, ctx) => {
try {
const { email, password } = await req.json();
const user = db.user.findFirst({ where: { email: { equals: email } } });
if (!user || user.password !== hash(password)) {
return res(ctx.status(401), ctx.json({ message: 'Invalid credentials' }));
}
const jwt = faker.string.uuid();
return res(ctx.json({ jwt, user: omit(user, ['password']) }));
} catch (error) {
return res(ctx.status(500), ctx.json({ message: 'Server Error' }));
}
}),
];
🚀 CI/CD集成策略
测试脚本配置
{
"scripts": {
"test": "vitest",
"test:unit": "vitest --config vitest.config.unit.ts",
"test:integration": "vitest --config vitest.config.integration.ts",
"test:e2e": "pm2 start \"yarn run-mock-server\" --name server && playwright test",
"test:ci": "yarn test:unit && yarn test:integration && yarn test:e2e",
"test:watch": "vitest --watch",
"test:coverage": "vitest --coverage"
}
}
分层测试执行流程
📈 测试覆盖率与质量指标
覆盖率目标设定
| 测试类型 | 覆盖率目标 | 关键指标 |
|---|---|---|
| 单元测试 | 80%+ | 组件逻辑覆盖率 |
| 集成测试 | 70%+ | 用户交互覆盖率 |
| 端到端测试 | 50%+ | 核心流程覆盖率 |
质量门禁配置
# 在CI pipeline中设置质量门禁
npm test -- --coverage --coverageThreshold='{
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}'
🎯 最佳实践总结
1. 测试优先级策略
- 优先编写集成测试:覆盖用户真实交互场景
- 关键业务单元测试:核心逻辑必须单元测试
- 端到端测试精选:只测试最重要的用户旅程
2. 测试数据管理
- 使用工厂函数创建测试数据
- 保持测试数据的一致性
- 避免硬编码的测试数据
3. 测试维护策略
- 定期重构测试代码
- 删除过时和重复的测试
- 保持测试代码的可读性
4. 性能优化
- 并行执行测试用例
- 使用测试双精度(Test Doubles)
- 避免不必要的渲染和网络请求
通过这套完整的测试策略,bulletproof-react项目能够确保代码质量,提高开发效率,并为持续交付提供坚实保障。记住:好的测试不是负担,而是开发效率的倍增器。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



