docs: 添加E2E测试统一方案设计文档
- 统一测试框架:从Python+Pytest迁移到TypeScript+Playwright - 建立五层测试体系:Smoke、Regression、Performance、Security、Accessibility - 设计分层CI/CD流程:快速验证、完整测试、全面测试 - 制定四阶段迁移计划:评估准备、核心迁移、专项补充、CI集成 - 金融级质量保障:满足安全、合规、可靠性要求
This commit is contained in:
@@ -0,0 +1,944 @@
|
||||
# Novalon Website E2E测试统一方案设计
|
||||
|
||||
**创建时间**: 2026-02-28
|
||||
**目标**: 统一E2E测试框架,从Python+Pytest迁移到TypeScript+Playwright,建立金融级网站完整的测试体系
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
### 当前状态
|
||||
- **两套测试框架并存**:
|
||||
- TypeScript + Playwright (`e2e/` 目录)
|
||||
- Python + Pytest (`e2e-tests/` 目录)
|
||||
- **维护成本高**: 需要维护两套技术栈
|
||||
- **测试覆盖不完整**: 部分测试场景缺失或重复
|
||||
|
||||
### 核心目标
|
||||
1. **统一技术栈**: 完全迁移到Playwright,与Next.js项目技术栈一致
|
||||
2. **全面测试覆盖**: 建立业务流程、性能、安全、可访问性完整测试体系
|
||||
3. **金融级质量**: 满足金融行业对安全、合规、可靠性的高要求
|
||||
4. **CI/CD集成**: 建立分层测试流程,平衡速度和覆盖率
|
||||
|
||||
---
|
||||
|
||||
## 二、技术选型决策
|
||||
|
||||
### 选择: TypeScript + Playwright
|
||||
|
||||
**决策理由**:
|
||||
- ✅ 与项目技术栈一致(Next.js + TypeScript)
|
||||
- ✅ 可共享类型定义,更好的类型安全
|
||||
- ✅ Playwright功能强大,支持多浏览器、移动端、视觉测试
|
||||
- ✅ 社区活跃,文档完善,生态成熟
|
||||
- ✅ 原生支持并行执行,性能优异
|
||||
|
||||
**放弃: Python + Pytest**
|
||||
- ❌ 与项目技术栈不一致,增加维护成本
|
||||
- ❌ 无法共享类型定义,降低开发效率
|
||||
- ❌ 需要维护两套依赖和环境
|
||||
|
||||
---
|
||||
|
||||
## 三、整体架构设计
|
||||
|
||||
### 3.1 分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 报告与监控层 (Reports & Monitoring) │
|
||||
│ HTML报告 | JSON/JUnit报告 | 性能指标 | 趋势分析 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 测试用例层 (Test Cases) │
|
||||
│ Smoke | Regression | Performance | Security │
|
||||
│ Accessibility | Visual | Mobile │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 页面对象层 (Page Objects) │
|
||||
│ BasePage | HomePage | ContactPage | Products │
|
||||
│ Components (Header, Footer, Form) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 基础设施层 (Infrastructure) │
|
||||
│ Playwright配置 | 测试数据 | 工具库 | Fixtures │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 目录结构
|
||||
|
||||
```
|
||||
e2e/
|
||||
├── src/
|
||||
│ ├── fixtures/ # 测试夹具
|
||||
│ │ ├── base.fixture.ts # 基础fixture
|
||||
│ │ ├── a11y.fixture.ts # 可访问性fixture
|
||||
│ │ └── auth.fixture.ts # 认证fixture(如需要)
|
||||
│ ├── pages/ # 页面对象
|
||||
│ │ ├── BasePage.ts # 基础页面
|
||||
│ │ ├── HomePage.ts # 首页
|
||||
│ │ ├── ContactPage.ts # 联系页面
|
||||
│ │ ├── ProductsPage.ts # 产品页面
|
||||
│ │ ├── ServicesPage.ts # 服务页面
|
||||
│ │ ├── AboutPage.ts # 关于页面
|
||||
│ │ ├── CasesPage.ts # 案例页面
|
||||
│ │ ├── SolutionsPage.ts # 解决方案页面
|
||||
│ │ └── NewsPage.ts # 新闻页面
|
||||
│ ├── tests/ # 测试用例
|
||||
│ │ ├── smoke/ # 冒烟测试
|
||||
│ │ ├── regression/ # 回归测试
|
||||
│ │ ├── performance/ # 性能测试
|
||||
│ │ ├── security/ # 安全测试
|
||||
│ │ ├── accessibility/ # 可访问性测试
|
||||
│ │ ├── visual/ # 视觉回归测试
|
||||
│ │ ├── mobile/ # 移动端测试
|
||||
│ │ ├── responsive/ # 响应式测试
|
||||
│ │ └── error-handling/ # 错误处理测试
|
||||
│ ├── types/ # 类型定义
|
||||
│ │ └── index.ts # 共享类型
|
||||
│ └── utils/ # 工具库
|
||||
│ ├── PerformanceMonitor.ts # 性能监控
|
||||
│ ├── TestDataGenerator.ts # 测试数据生成
|
||||
│ ├── devices.ts # 设备配置
|
||||
│ └── helpers.ts # 辅助函数
|
||||
├── test-data/ # 测试数据文件
|
||||
│ ├── contact-form.json # 联系表单数据
|
||||
│ ├── products.json # 产品数据
|
||||
│ └── performance-budgets.json # 性能预算
|
||||
├── playwright.config.ts # Playwright配置
|
||||
├── package.json # 依赖管理
|
||||
└── tsconfig.json # TypeScript配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、测试分层策略
|
||||
|
||||
### 4.1 测试金字塔
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
│ Security│ (每周/发布前)
|
||||
│ Access │ 10-15分钟
|
||||
└─────────┘
|
||||
┌───────────────┐
|
||||
│ Performance │ (每日/发布前)
|
||||
│ 10-20分钟 │
|
||||
└───────────────┘
|
||||
┌─────────────────────┐
|
||||
│ Regression │ (PR合并前/每日)
|
||||
│ 15-30分钟 │
|
||||
└─────────────────────┘
|
||||
┌───────────────────────────┐
|
||||
│ Smoke │ (每次提交)
|
||||
│ < 5分钟 │
|
||||
└───────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 各层测试详解
|
||||
|
||||
#### Level 1: Smoke Tests(冒烟测试)
|
||||
|
||||
**执行频率**: 每次代码提交
|
||||
**执行时间**: < 5分钟
|
||||
**标签**: `@smoke`
|
||||
|
||||
**覆盖范围**:
|
||||
- ✅ 所有关键页面可访问性(首页、产品、服务、联系)
|
||||
- ✅ 核心导航功能
|
||||
- ✅ 关键表单提交(联系表单)
|
||||
- ✅ 页面基本渲染(无JS错误)
|
||||
|
||||
**测试用例**:
|
||||
```typescript
|
||||
test.describe('Smoke Tests @smoke', () => {
|
||||
test('首页可访问', async ({ page }) => {
|
||||
await homePage.goto();
|
||||
await expect(page).toHaveTitle(/Novalon/);
|
||||
});
|
||||
|
||||
test('导航功能正常', async ({ page }) => {
|
||||
await homePage.goto();
|
||||
await homePage.navigateTo('产品');
|
||||
await expect(page).toHaveURL(/products/);
|
||||
});
|
||||
|
||||
test('联系表单可提交', async ({ page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.fillForm({
|
||||
name: '测试用户',
|
||||
email: 'test@example.com',
|
||||
message: '测试消息'
|
||||
});
|
||||
await contactPage.submit();
|
||||
await expect(contactPage.successMessage).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Level 2: Regression Tests(回归测试)
|
||||
|
||||
**执行频率**: PR合并前、每日构建
|
||||
**执行时间**: 15-30分钟
|
||||
**标签**: `@regression`
|
||||
|
||||
**覆盖范围**:
|
||||
- ✅ 所有业务流程完整测试
|
||||
- ✅ 表单验证(必填项、格式校验、错误提示)
|
||||
- ✅ 页面间跳转和数据传递
|
||||
- ✅ 响应式布局(桌面/平板/移动端)
|
||||
- ✅ 多浏览器兼容性
|
||||
|
||||
**测试用例**:
|
||||
```typescript
|
||||
test.describe('Contact Form Regression @regression', () => {
|
||||
test('表单验证 - 必填项', async ({ page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.submit();
|
||||
await expect(contactPage.nameError).toHaveText('请输入姓名');
|
||||
await expect(contactPage.emailError).toHaveText('请输入邮箱');
|
||||
});
|
||||
|
||||
test('表单验证 - 邮箱格式', async ({ page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.fillEmail('invalid-email');
|
||||
await contactPage.submit();
|
||||
await expect(contactPage.emailError).toHaveText('邮箱格式不正确');
|
||||
});
|
||||
|
||||
test('表单提交成功', async ({ page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.fillForm(testData.validContact);
|
||||
await contactPage.submit();
|
||||
await expect(contactPage.successMessage).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Level 3: Performance Tests(性能测试)
|
||||
|
||||
**执行频率**: 每日构建、发布前
|
||||
**执行时间**: 10-20分钟
|
||||
**标签**: `@performance`
|
||||
|
||||
**覆盖范围**:
|
||||
- ✅ 页面加载时间(FCP、LCP、TTI)
|
||||
- ✅ 资源大小优化(图片、JS、CSS)
|
||||
- ✅ 网络请求数量和瀑布流
|
||||
- ✅ 核心交互响应时间
|
||||
- ✅ 性能预算验证
|
||||
|
||||
**性能预算**:
|
||||
```json
|
||||
{
|
||||
"performanceBudgets": {
|
||||
"home": {
|
||||
"loadTime": 3000,
|
||||
"fcP": 1500,
|
||||
"lcp": 2500,
|
||||
"tti": 3500,
|
||||
"totalSize": 1500000
|
||||
},
|
||||
"contact": {
|
||||
"loadTime": 2500,
|
||||
"fcp": 1200,
|
||||
"lcp": 2000,
|
||||
"tti": 3000,
|
||||
"totalSize": 1200000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**测试用例**:
|
||||
```typescript
|
||||
test.describe('Performance Tests @performance', () => {
|
||||
test('首页性能预算', async ({ page }) => {
|
||||
const metrics = await performanceMonitor.measurePageLoad(page, '/');
|
||||
|
||||
expect(metrics.loadTime).toBeLessThan(budgets.home.loadTime);
|
||||
expect(metrics.fcp).toBeLessThan(budgets.home.fcp);
|
||||
expect(metrics.lcp).toBeLessThan(budgets.home.lcp);
|
||||
});
|
||||
|
||||
test('图片优化验证', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const images = await page.$$eval('img', imgs =>
|
||||
imgs.map(img => ({
|
||||
src: img.src,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
displayWidth: img.width,
|
||||
displayHeight: img.height
|
||||
}))
|
||||
);
|
||||
|
||||
for (const img of images) {
|
||||
expect(img.naturalWidth).toBeLessThanOrEqual(img.displayWidth * 2);
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Level 4: Security Tests(安全测试)
|
||||
|
||||
**执行频率**: 每周、发布前
|
||||
**执行时间**: 10-15分钟
|
||||
**标签**: `@security`
|
||||
|
||||
**覆盖范围**:
|
||||
- ✅ XSS漏洞检测(表单输入)
|
||||
- ✅ CSRF保护验证
|
||||
- ✅ 安全头验证(CSP、HSTS等)
|
||||
- ✅ 敏感数据处理(联系表单数据加密)
|
||||
- ✅ HTTPS强制跳转
|
||||
|
||||
**测试用例**:
|
||||
```typescript
|
||||
test.describe('Security Tests @security', () => {
|
||||
test('XSS防护 - 联系表单', async ({ page }) => {
|
||||
await contactPage.goto();
|
||||
await contactPage.fillForm({
|
||||
name: '<script>alert("XSS")</script>',
|
||||
email: 'test@example.com',
|
||||
message: '<img src=x onerror=alert("XSS")>'
|
||||
});
|
||||
await contactPage.submit();
|
||||
|
||||
const pageContent = await page.content();
|
||||
expect(pageContent).not.toContain('<script>alert');
|
||||
expect(pageContent).not.toContain('onerror=alert');
|
||||
});
|
||||
|
||||
test('安全头验证', async ({ page }) => {
|
||||
const response = await page.goto('/');
|
||||
const headers = response.headers();
|
||||
|
||||
expect(headers['x-frame-options']).toBeDefined();
|
||||
expect(headers['x-content-type-options']).toBe('nosniff');
|
||||
expect(headers['strict-transport-security']).toBeDefined();
|
||||
});
|
||||
|
||||
test('HTTPS强制跳转', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
expect(page.url()).toMatch(/^https:/);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Level 5: Accessibility Tests(可访问性测试)
|
||||
|
||||
**执行频率**: 每周、发布前
|
||||
**执行时间**: 10-15分钟
|
||||
**标签**: `@accessibility`
|
||||
|
||||
**覆盖范围**:
|
||||
- ✅ WCAG 2.1 AA标准合规
|
||||
- ✅ 键盘导航测试
|
||||
- ✅ 屏幕阅读器兼容性
|
||||
- ✅ 颜色对比度验证
|
||||
- ✅ 表单标签和ARIA属性
|
||||
|
||||
**测试用例**:
|
||||
```typescript
|
||||
test.describe('Accessibility Tests @accessibility', () => {
|
||||
test('WCAG 2.1 AA合规 - 首页', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa'])
|
||||
.analyze();
|
||||
|
||||
expect(accessibilityScanResults.violations).toEqual([]);
|
||||
});
|
||||
|
||||
test('键盘导航', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.locator(':focus')).toBeVisible();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await page.keyboard.press('Tab');
|
||||
const focusedElement = page.locator(':focus');
|
||||
await expect(focusedElement).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('颜色对比度', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const contrastResults = await new AxeBuilder({ page })
|
||||
.withRules(['color-contrast'])
|
||||
.analyze();
|
||||
|
||||
expect(contrastResults.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、迁移计划与实施步骤
|
||||
|
||||
### 5.1 迁移阶段划分
|
||||
|
||||
#### 阶段1: 评估与准备(1-2天)
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 分析现有Python测试用例,识别核心测试场景
|
||||
- [ ] 评估现有Playwright测试的完整性和质量
|
||||
- [ ] 确定需要迁移、重构、新增的测试用例
|
||||
- [ ] 制定详细的测试用例清单和优先级
|
||||
- [ ] 准备测试数据和测试环境配置
|
||||
|
||||
**产出物**:
|
||||
- 测试用例迁移清单(Excel/Markdown)
|
||||
- 测试覆盖率分析报告
|
||||
- 风险评估文档
|
||||
|
||||
#### 阶段2: 核心测试迁移(3-5天)
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 迁移smoke测试(确保基础功能覆盖)
|
||||
- [ ] 页面可访问性测试
|
||||
- [ ] 导航功能测试
|
||||
- [ ] 关键表单测试
|
||||
- [ ] 迁移regression测试(核心业务流程)
|
||||
- [ ] 联系表单完整测试
|
||||
- [ ] 产品展示测试
|
||||
- [ ] 服务流程测试
|
||||
- [ ] 建立完整的页面对象模型
|
||||
- [ ] 配置测试数据管理
|
||||
|
||||
**产出物**:
|
||||
- 完整的页面对象模型
|
||||
- Smoke测试套件
|
||||
- Regression测试套件
|
||||
- 测试数据文件
|
||||
|
||||
#### 阶段3: 专项测试补充(3-4天)
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 补充performance测试(性能监控)
|
||||
- [ ] 页面加载性能测试
|
||||
- [ ] 资源优化验证
|
||||
- [ ] 性能预算测试
|
||||
- [ ] 补充security测试(安全合规)
|
||||
- [ ] XSS漏洞测试
|
||||
- [ ] 安全头验证
|
||||
- [ ] HTTPS验证
|
||||
- [ ] 补充accessibility测试(无障碍访问)
|
||||
- [ ] WCAG合规测试
|
||||
- [ ] 键盘导航测试
|
||||
- [ ] 颜色对比度测试
|
||||
- [ ] 补充visual regression测试(视觉回归)
|
||||
- [ ] 关键页面截图对比
|
||||
- [ ] 组件视觉测试
|
||||
|
||||
**产出物**:
|
||||
- Performance测试套件
|
||||
- Security测试套件
|
||||
- Accessibility测试套件
|
||||
- Visual regression测试套件
|
||||
|
||||
#### 阶段4: CI/CD集成与清理(2-3天)
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 配置Woodpecker CI流程
|
||||
- [ ] 快速验证流程(每次提交)
|
||||
- [ ] 完整测试流程(PR合并前)
|
||||
- [ ] 定时测试流程(每日构建)
|
||||
- [ ] 建立测试报告和通知机制
|
||||
- [ ] HTML报告生成
|
||||
- [ ] 失败通知配置
|
||||
- [ ] 性能告警配置
|
||||
- [ ] 删除Python测试框架(`e2e-tests/`目录)
|
||||
- [ ] 更新项目文档
|
||||
- [ ] README更新
|
||||
- [ ] 测试指南编写
|
||||
- [ ] CI/CD文档
|
||||
|
||||
**产出物**:
|
||||
- Woodpecker CI配置文件
|
||||
- 测试报告系统
|
||||
- 更新的项目文档
|
||||
|
||||
### 5.2 迁移优先级矩阵
|
||||
|
||||
| 测试类型 | 优先级 | 迁移阶段 | 说明 |
|
||||
|---------|--------|---------|------|
|
||||
| 页面可访问性 | 高 | 阶段2 | 核心功能,必须覆盖 |
|
||||
| 导航功能 | 高 | 阶段2 | 核心功能,必须覆盖 |
|
||||
| 联系表单 | 高 | 阶段2 | 核心业务,必须覆盖 |
|
||||
| 表单验证 | 高 | 阶段2 | 核心业务,必须覆盖 |
|
||||
| 响应式测试 | 中 | 阶段2 | 重要但可后续优化 |
|
||||
| 性能测试 | 中 | 阶段3 | 专项测试,独立实施 |
|
||||
| 安全测试 | 中 | 阶段3 | 专项测试,独立实施 |
|
||||
| 可访问性测试 | 中 | 阶段3 | 专项测试,独立实施 |
|
||||
| 视觉回归 | 低 | 阶段3 | 可选,根据需求决定 |
|
||||
|
||||
### 5.3 风险控制
|
||||
|
||||
**风险识别**:
|
||||
1. **测试覆盖不足**: 迁移过程中可能遗漏测试场景
|
||||
2. **测试不稳定**: 新测试可能存在时序问题或环境依赖
|
||||
3. **性能下降**: 测试执行时间可能超出预期
|
||||
4. **CI流程中断**: 配置错误可能导致CI失败
|
||||
|
||||
**缓解措施**:
|
||||
1. **保留Python测试**: 在Playwright测试完全覆盖前,保留Python测试作为备份
|
||||
2. **渐进式迁移**: 分阶段迁移,每个阶段完成后进行验证
|
||||
3. **并行执行**: 在迁移期间,两套框架并行运行,对比测试结果
|
||||
4. **回滚机制**: 准备回滚方案,出现问题可快速恢复
|
||||
|
||||
---
|
||||
|
||||
## 六、CI/CD集成方案
|
||||
|
||||
### 6.1 Woodpecker CI流程设计
|
||||
|
||||
#### Level 1: 快速验证(每次提交触发)
|
||||
|
||||
```yaml
|
||||
# .woodpecker.yml
|
||||
when:
|
||||
event: [push, pull_request]
|
||||
|
||||
steps:
|
||||
- name: install-dependencies
|
||||
image: node:20
|
||||
commands:
|
||||
- cd e2e
|
||||
- npm ci
|
||||
|
||||
- name: smoke-tests
|
||||
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
||||
environment:
|
||||
CI: true
|
||||
commands:
|
||||
- cd e2e
|
||||
- npx playwright test --grep @smoke --project=chromium
|
||||
when:
|
||||
status: success
|
||||
|
||||
- name: upload-results
|
||||
image: plugins/s3
|
||||
settings:
|
||||
bucket: test-results
|
||||
source: e2e/test-results/**/*
|
||||
target: /${CI_BUILD_NUMBER}/
|
||||
when:
|
||||
status: [success, failure]
|
||||
```
|
||||
|
||||
**执行时间**: < 5分钟
|
||||
**触发条件**: 每次代码提交
|
||||
**失败处理**: 阻止合并,发送通知
|
||||
|
||||
#### Level 2: 完整测试(PR合并前触发)
|
||||
|
||||
```yaml
|
||||
when:
|
||||
event: pull_request
|
||||
branch: main
|
||||
|
||||
steps:
|
||||
- name: regression-tests
|
||||
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
||||
environment:
|
||||
CI: true
|
||||
commands:
|
||||
- cd e2e
|
||||
- npx playwright test --grep @regression
|
||||
# 执行时间:15-30分钟
|
||||
|
||||
- name: performance-tests
|
||||
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
||||
commands:
|
||||
- cd e2e
|
||||
- npx playwright test --grep @performance
|
||||
# 执行时间:10-20分钟
|
||||
|
||||
- name: generate-report
|
||||
image: node:20
|
||||
commands:
|
||||
- cd e2e
|
||||
- npx playwright show-report --host 0.0.0.0
|
||||
```
|
||||
|
||||
**执行时间**: 25-50分钟
|
||||
**触发条件**: PR合并前
|
||||
**失败处理**: 阻止合并,生成详细报告
|
||||
|
||||
#### Level 3: 全面测试(定时执行)
|
||||
|
||||
```yaml
|
||||
when:
|
||||
event: cron
|
||||
cron: "0 2 * * *" # 每天凌晨2点
|
||||
|
||||
steps:
|
||||
- name: full-test-suite
|
||||
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
||||
environment:
|
||||
CI: true
|
||||
commands:
|
||||
- cd e2e
|
||||
- npx playwright test
|
||||
# 执行完整测试套件
|
||||
|
||||
- name: security-tests
|
||||
commands:
|
||||
- npx playwright test --grep @security
|
||||
|
||||
- name: accessibility-tests
|
||||
commands:
|
||||
- npx playwright test --grep @accessibility
|
||||
|
||||
- name: generate-trend-report
|
||||
commands:
|
||||
- node scripts/generate-trend-report.js
|
||||
```
|
||||
|
||||
**执行时间**: 1-2小时
|
||||
**触发条件**: 每日凌晨2点
|
||||
**失败处理**: 发送通知,生成趋势报告
|
||||
|
||||
### 6.2 测试报告与通知
|
||||
|
||||
#### 报告生成
|
||||
|
||||
**HTML报告**:
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
reporter: [
|
||||
['html', {
|
||||
outputFolder: 'test-results/html-report',
|
||||
open: 'never'
|
||||
}],
|
||||
['json', {
|
||||
outputFile: 'test-results/results.json'
|
||||
}],
|
||||
['junit', {
|
||||
outputFile: 'test-results/junit.xml'
|
||||
}],
|
||||
['list']
|
||||
]
|
||||
```
|
||||
|
||||
**报告内容**:
|
||||
- 测试执行摘要(通过/失败/跳过)
|
||||
- 失败测试详情(截图、视频、trace)
|
||||
- 性能指标趋势
|
||||
- 测试覆盖率统计
|
||||
|
||||
#### 通知机制
|
||||
|
||||
**失败通知**:
|
||||
```yaml
|
||||
- name: notify-failure
|
||||
image: plugins/slack
|
||||
settings:
|
||||
webhook: ${SLACK_WEBHOOK}
|
||||
channel: testing
|
||||
template: |
|
||||
❌ E2E测试失败
|
||||
|
||||
构建号: {{build.number}}
|
||||
分支: {{build.branch}}
|
||||
提交: {{build.commit}}
|
||||
|
||||
失败测试: {{failures}}
|
||||
报告链接: {{report_url}}
|
||||
when:
|
||||
status: failure
|
||||
```
|
||||
|
||||
**性能告警**:
|
||||
```typescript
|
||||
// scripts/check-performance-regression.ts
|
||||
const currentMetrics = await getCurrentMetrics();
|
||||
const baselineMetrics = await getBaselineMetrics();
|
||||
|
||||
if (currentMetrics.loadTime > baselineMetrics.loadTime * 1.2) {
|
||||
await sendAlert({
|
||||
type: 'performance_regression',
|
||||
message: `页面加载时间退化20%: ${currentMetrics.loadTime}ms vs ${baselineMetrics.loadTime}ms`
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 质量门禁
|
||||
|
||||
#### PR合并条件
|
||||
|
||||
**必须满足**:
|
||||
- ✅ Smoke测试100%通过
|
||||
- ✅ Regression测试通过率 ≥ 95%
|
||||
- ✅ 无新增严重缺陷
|
||||
- ✅ 性能指标在预算范围内
|
||||
|
||||
**可选条件**:
|
||||
- ⚠️ Performance测试通过率 ≥ 90%
|
||||
- ⚠️ 测试覆盖率 ≥ 80%
|
||||
|
||||
#### 质量指标跟踪
|
||||
|
||||
```typescript
|
||||
// scripts/quality-metrics.ts
|
||||
interface QualityMetrics {
|
||||
smokePassRate: number; // 目标: 100%
|
||||
regressionPassRate: number; // 目标: ≥ 95%
|
||||
performanceBudget: number; // 目标: 100%符合预算
|
||||
testCoverage: number; // 目标: ≥ 80%
|
||||
avgExecutionTime: number; // 目标: < 30分钟
|
||||
}
|
||||
|
||||
async function checkQualityGate(metrics: QualityMetrics): Promise<boolean> {
|
||||
return (
|
||||
metrics.smokePassRate === 100 &&
|
||||
metrics.regressionPassRate >= 95 &&
|
||||
metrics.performanceBudget === 100 &&
|
||||
metrics.testCoverage >= 80
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、测试数据管理
|
||||
|
||||
### 7.1 测试数据策略
|
||||
|
||||
**数据源分类**:
|
||||
1. **静态测试数据**: JSON/YAML文件,存储在`test-data/`目录
|
||||
2. **动态测试数据**: 运行时生成,使用TestDataGenerator
|
||||
3. **环境特定数据**: 通过环境变量配置
|
||||
|
||||
### 7.2 数据文件示例
|
||||
|
||||
**联系表单测试数据** (`test-data/contact-form.json`):
|
||||
```json
|
||||
{
|
||||
"valid": {
|
||||
"name": "张三",
|
||||
"email": "zhangsan@example.com",
|
||||
"phone": "13800138000",
|
||||
"company": "测试公司",
|
||||
"message": "这是一条测试消息"
|
||||
},
|
||||
"invalid": {
|
||||
"emptyName": {
|
||||
"name": "",
|
||||
"email": "test@example.com",
|
||||
"message": "测试消息",
|
||||
"expectedError": "请输入姓名"
|
||||
},
|
||||
"invalidEmail": {
|
||||
"name": "测试用户",
|
||||
"email": "invalid-email",
|
||||
"message": "测试消息",
|
||||
"expectedError": "邮箱格式不正确"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 数据生成器
|
||||
|
||||
```typescript
|
||||
// src/utils/TestDataGenerator.ts
|
||||
export class TestDataGenerator {
|
||||
static generateContactForm(overrides = {}) {
|
||||
return {
|
||||
name: `测试用户_${Date.now()}`,
|
||||
email: `test_${Date.now()}@example.com`,
|
||||
phone: this.generatePhone(),
|
||||
company: '测试公司',
|
||||
message: `测试消息_${Date.now()}`,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
static generatePhone() {
|
||||
return `1${Math.floor(Math.random() * 9000000000 + 1000000000)}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、最佳实践与规范
|
||||
|
||||
### 8.1 测试编写规范
|
||||
|
||||
**命名规范**:
|
||||
```typescript
|
||||
// ✅ 好的命名
|
||||
test('联系表单 - 提交成功', async ({ page }) => {});
|
||||
test('联系表单 - 邮箱格式验证', async ({ page }) => {});
|
||||
|
||||
// ❌ 不好的命名
|
||||
test('test1', async ({ page }) => {});
|
||||
test('表单测试', async ({ page }) => {});
|
||||
```
|
||||
|
||||
**测试结构**:
|
||||
```typescript
|
||||
test.describe('功能模块 @tag', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 前置条件
|
||||
});
|
||||
|
||||
test('测试场景 - 具体描述', async ({ page }) => {
|
||||
// Arrange: 准备测试数据
|
||||
const testData = TestDataGenerator.generateContactForm();
|
||||
|
||||
// Act: 执行测试操作
|
||||
await contactPage.fillForm(testData);
|
||||
await contactPage.submit();
|
||||
|
||||
// Assert: 验证结果
|
||||
await expect(contactPage.successMessage).toBeVisible();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// 清理工作
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 8.2 页面对象模式
|
||||
|
||||
**BasePage示例**:
|
||||
```typescript
|
||||
// src/pages/BasePage.ts
|
||||
export abstract class BasePage {
|
||||
constructor(protected page: Page) {}
|
||||
|
||||
async goto(path: string = '/') {
|
||||
await this.page.goto(path);
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async waitForPageLoad() {
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async screenshot(name: string) {
|
||||
await this.page.screenshot({
|
||||
path: `screenshots/${name}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
|
||||
async waitForElement(selector: string, timeout = 30000) {
|
||||
await this.page.waitForSelector(selector, { timeout });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ContactPage示例**:
|
||||
```typescript
|
||||
// src/pages/ContactPage.ts
|
||||
export class ContactPage extends BasePage {
|
||||
readonly nameInput = this.page.locator('input[name="name"]');
|
||||
readonly emailInput = this.page.locator('input[name="email"]');
|
||||
readonly messageInput = this.page.locator('textarea[name="message"]');
|
||||
readonly submitButton = this.page.locator('button[type="submit"]');
|
||||
readonly successMessage = this.page.locator('.success-message');
|
||||
readonly nameError = this.page.locator('.error-name');
|
||||
readonly emailError = this.page.locator('.error-email');
|
||||
|
||||
async goto() {
|
||||
await super.goto('/contact');
|
||||
}
|
||||
|
||||
async fillForm(data: ContactFormData) {
|
||||
await this.nameInput.fill(data.name);
|
||||
await this.emailInput.fill(data.email);
|
||||
if (data.phone) await this.page.locator('input[name="phone"]').fill(data.phone);
|
||||
if (data.company) await this.page.locator('input[name="company"]').fill(data.company);
|
||||
await this.messageInput.fill(data.message);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.submitButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 断言最佳实践
|
||||
|
||||
```typescript
|
||||
// ✅ 好的断言
|
||||
await expect(page).toHaveURL(/contact/);
|
||||
await expect(element).toBeVisible();
|
||||
await expect(element).toHaveText('预期文本');
|
||||
await expect(element).toHaveAttribute('href', '/expected-path');
|
||||
|
||||
// ❌ 不好的断言
|
||||
expect(await element.isVisible()).toBe(true);
|
||||
expect(await element.textContent()).toContain('文本');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、监控与持续改进
|
||||
|
||||
### 9.1 测试质量指标
|
||||
|
||||
**关键指标**:
|
||||
- 测试通过率
|
||||
- 测试覆盖率
|
||||
- 平均执行时间
|
||||
- Flaky测试率
|
||||
- 缺陷逃逸率
|
||||
|
||||
### 9.2 持续改进计划
|
||||
|
||||
**定期评估**:
|
||||
- 每周回顾测试结果,识别不稳定测试
|
||||
- 每月分析测试覆盖率,补充缺失场景
|
||||
- 每季度评估测试策略,优化测试金字塔
|
||||
|
||||
**优化方向**:
|
||||
- 减少测试执行时间
|
||||
- 提高测试稳定性
|
||||
- 增强测试覆盖率
|
||||
- 改进测试报告
|
||||
|
||||
---
|
||||
|
||||
## 十、总结
|
||||
|
||||
### 核心价值
|
||||
|
||||
1. **统一技术栈**: 降低维护成本,提高开发效率
|
||||
2. **全面覆盖**: 业务、性能、安全、可访问性全覆盖
|
||||
3. **金融级质量**: 满足金融行业高标准要求
|
||||
4. **CI/CD集成**: 自动化测试流程,快速反馈
|
||||
|
||||
### 预期收益
|
||||
|
||||
- **开发效率**: 减少30%的测试维护时间
|
||||
- **质量保障**: 缺陷逃逸率降低50%
|
||||
- **发布速度**: 测试反馈时间缩短60%
|
||||
- **团队协作**: 统一技术栈,降低学习成本
|
||||
|
||||
### 下一步行动
|
||||
|
||||
1. ✅ 完成设计文档评审
|
||||
2. ⏭️ 开始阶段1:评估与准备
|
||||
3. ⏭️ 按计划推进迁移工作
|
||||
4. ⏭️ 建立CI/CD流程
|
||||
5. ⏭️ 持续优化和改进
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**最后更新**: 2026-02-28
|
||||
**维护者**: 张翔
|
||||
Reference in New Issue
Block a user