|
@@ -0,0 +1,535 @@
|
|
|
|
|
+# Epic 9 模式总结:E2E 测试最佳实践
|
|
|
|
|
+
|
|
|
|
|
+**创建日期:** 2026-01-12
|
|
|
|
|
+**Epic:** Epic 9 - 残疾人管理完整 E2E 测试覆盖
|
|
|
|
|
+**目的:** 为后续 Epic 提供可复用的测试模式和最佳实践
|
|
|
|
|
+
|
|
|
|
|
+## 目录
|
|
|
|
|
+
|
|
|
|
|
+1. [测试数据隔离模式](#1-测试数据隔离模式)
|
|
|
|
|
+2. [并行执行配置](#2-并行执行配置)
|
|
|
|
|
+3. [Page Object 方法设计模式](#3-page-object-方法设计模式)
|
|
|
|
|
+4. [代码审查常见问题检查清单](#4-代码审查常见问题检查清单)
|
|
|
|
|
+5. [稳定性测试模式](#5-稳定性测试模式)
|
|
|
|
|
+6. [测试文件组织结构](#6-测试文件组织结构)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 1. 测试数据隔离模式
|
|
|
|
|
+
|
|
|
|
|
+### 1.1 时间戳前缀模式(标准模式)
|
|
|
|
|
+
|
|
|
|
|
+**目的:** 确保每个测试使用唯一数据,避免并发测试时的数据冲突
|
|
|
|
|
+
|
|
|
|
|
+**实现方式:**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+test.describe('残疾人管理 - 功能测试', () => {
|
|
|
|
|
+ // 在 test.describe 级别声明时间戳
|
|
|
|
|
+ const TEST_TIMESTAMP = Date.now();
|
|
|
|
|
+ const TEST_PREFIX = `test_${TEST_TIMESTAMP}`;
|
|
|
|
|
+
|
|
|
|
|
+ test('应该成功添加数据', async ({ page }) => {
|
|
|
|
|
+ const uniqueName = `${TEST_PREFIX}_数据A`;
|
|
|
|
|
+ // 使用 uniqueName 创建数据
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('应该成功编辑数据', async ({ page }) => {
|
|
|
|
|
+ const uniqueName = `${TEST_PREFIX}_数据B`;
|
|
|
|
|
+ // 使用 uniqueName 创建数据
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**关键点:**
|
|
|
|
|
+- 时间戳在 `test.describe` 级别声明,避免全局污染
|
|
|
|
|
+- 每个测试使用不同的后缀(`_数据A`, `_数据B`)确保数据唯一性
|
|
|
|
|
+- 所有测试可以并行运行,不会发生数据冲突
|
|
|
|
|
+
|
|
|
|
|
+### 1.2 唯一数据追踪数组
|
|
|
|
|
+
|
|
|
|
|
+**目的:** 跟踪测试创建的数据,方便在 `afterEach` 中清理
|
|
|
|
|
+
|
|
|
|
|
+**实现方式:**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+test.describe('残疾人管理 - 功能测试', () => {
|
|
|
|
|
+ const TEST_TIMESTAMP = Date.now();
|
|
|
|
|
+ const TEST_PREFIX = `test_${TEST_TIMESTAMP}`;
|
|
|
|
|
+ // 在 describe 级别声明数组,避免全局污染
|
|
|
|
|
+ const createdTestData: Array<{ name: string }> = [];
|
|
|
|
|
+
|
|
|
|
|
+ test('应该成功添加数据', async ({ page }) => {
|
|
|
|
|
+ const testData = { name: `${TEST_PREFIX}_数据A` };
|
|
|
|
|
+ await pageObject.create(testData);
|
|
|
|
|
+ // 记录创建的数据
|
|
|
|
|
+ createdTestData.push(testData);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test.afterEach(async ({ disabilityPersonPage, page }) => {
|
|
|
|
|
+ // 清理本次测试创建的所有数据
|
|
|
|
|
+ for (const data of createdTestData) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await disabilityPersonPage.goto();
|
|
|
|
|
+ await disabilityPersonPage.searchByName(data.name);
|
|
|
|
|
+ const deleteButton = page.getByRole('button', { name: '删除' }).first();
|
|
|
|
|
+ if (await deleteButton.count({ timeout: 2000 }) > 0) {
|
|
|
|
|
+ await deleteButton.click({ timeout: 5000 });
|
|
|
|
|
+ await page.getByRole('button', { name: '确认' }).click({ timeout: 5000 }).catch(() => {});
|
|
|
|
|
+ await page.waitForTimeout(500);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.debug(` ⚠ 清理数据失败: ${data.name}`, error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**关键点:**
|
|
|
|
|
+- `createdTestData` 数组在 `test.describe` 级别声明
|
|
|
|
|
+- 每个测试完成后添加到数组
|
|
|
|
|
+- `afterEach` 清理数组中的所有数据
|
|
|
|
|
+- 使用 try-catch 确保单个清理失败不影响其他清理
|
|
|
|
|
+
|
|
|
|
|
+### 1.3 随机数范围优化
|
|
|
|
|
+
|
|
|
|
|
+**目的:** 使用更大的随机数范围避免并发冲突
|
|
|
|
|
+
|
|
|
|
|
+**错误方式(Epic 9 早期):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ❌ 6 位随机数范围太小,并发时可能冲突
|
|
|
|
|
+const randomId = Math.floor(Math.random() * 1000000);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**正确方式(Epic 9 改进):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ✅ 使用 Number.MAX_SAFE_INTEGER 确保唯一性
|
|
|
|
|
+const uniqueId = Number.MAX_SAFE_INTEGER - TEST_TIMESTAMP;
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 1.4 省市选择多样化(可选)
|
|
|
|
|
+
|
|
|
|
|
+**目的:** 避免所有测试使用相同的省市,提高测试覆盖真实性
|
|
|
|
|
+
|
|
|
|
|
+**问题:** 所有测试使用"湖北省/武汉市",虽然稳定但覆盖不全面
|
|
|
|
|
+
|
|
|
|
|
+**改进方案:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+const PROVINCE_CITY_MAP = [
|
|
|
|
|
+ { province: '北京市', city: '北京市' },
|
|
|
|
|
+ { province: '上海市', city: '上海市' },
|
|
|
|
|
+ { province: '广东省', city: '广州市' },
|
|
|
|
|
+ { province: '湖北省', city: '武汉市' },
|
|
|
|
|
+ { province: '四川省', city: '成都市' },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// 基于测试索引选择不同的省市
|
|
|
|
|
+const location = PROVINCE_CITY_MAP[testIndex % PROVINCE_CITY_MAP.length];
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**注意:** 需要确保并发时省市选择不会冲突(可以使用时间戳的最后一位作为索引)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 2. 并行执行配置
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 移除 test.describe.serial
|
|
|
|
|
+
|
|
|
|
|
+**问题:** `test.describe.serial` 会阻止并行执行,违背 Playwright 设计初衷
|
|
|
|
|
+
|
|
|
|
|
+**修改前(Epic 9 早期):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ❌ 串行执行,所有测试必须顺序执行
|
|
|
|
|
+test.describe.serial('残疾人管理 - 照片上传功能', () => {
|
|
|
|
|
+ // 测试...
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**修改后(Epic 9 改进):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ✅ 并行执行,多个测试可以同时运行
|
|
|
|
|
+test.describe('残疾人管理 - 照片上传功能', () => {
|
|
|
|
|
+ // 测试...
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2.2 Playwright 配置
|
|
|
|
|
+
|
|
|
|
|
+**默认配置(推荐):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// playwright.config.ts
|
|
|
|
|
+export default defineConfig({
|
|
|
|
|
+ fullyParallel: true, // 启用并行
|
|
|
|
|
+ workers: process.env.CI ? 1 : undefined, // CI 环境单 worker,本地环境自动检测
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**指定 worker 数量:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+export default defineConfig({
|
|
|
|
|
+ fullyParallel: true,
|
|
|
|
|
+ workers: 4, // 使用 4 个并行 worker
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 并行执行命令
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# 并行运行所有测试(默认)
|
|
|
|
|
+cd web
|
|
|
|
|
+pnpm test:e2e:chromium
|
|
|
|
|
+
|
|
|
|
|
+# 指定 4 个 worker
|
|
|
|
|
+pnpm test:e2e:chromium --workers=4
|
|
|
|
|
+
|
|
|
|
|
+# 指定单个文件并行运行
|
|
|
|
|
+pnpm test:e2e:chromium disability-person-photo.spec.ts --workers=2
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2.4 性能提升数据
|
|
|
|
|
+
|
|
|
|
|
+Epic 9 实测数据:
|
|
|
|
|
+
|
|
|
|
|
+| 配置 | 执行时间 | 提升 |
|
|
|
|
|
+|------|---------|------|
|
|
|
|
|
+| 1 worker (串行) | 3.3 分钟 | 基准 |
|
|
|
|
|
+| 2 workers | 2.2 分钟 | 1.5x |
|
|
|
|
|
+| 4 workers | 1.1 分钟 | **3x** |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 3. Page Object 方法设计模式
|
|
|
|
|
+
|
|
|
|
|
+### 3.1 方法命名规范
|
|
|
|
|
+
|
|
|
|
|
+**命名格式:** `动词+名词`,camelCase
|
|
|
|
|
+
|
|
|
|
|
+| 方法类型 | 命名示例 |
|
|
|
|
|
+|---------|---------|
|
|
|
|
|
+| 创建 | `createPerson()`, `addBankCard()`, `addNote()` |
|
|
|
|
|
+| 读取 | `getPerson()`, `getBankCardList()`, `getNoteCount()` |
|
|
|
|
|
+| 更新 | `updatePerson()`, `editBankCard()`, `editNote()` |
|
|
|
|
|
+| 删除 | `deletePerson()`, `deleteBankCard()`, `removeNote()` |
|
|
|
|
|
+| 检查 | `personExists()`, `isAddButtonDisabled()` |
|
|
|
|
|
+| 等待 | `waitForPersonExists()`, `waitForPersonNotExists()` |
|
|
|
|
|
+
|
|
|
|
|
+### 3.2 方法设计模板
|
|
|
|
|
+
|
|
|
|
|
+**创建类方法模板:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+/**
|
|
|
|
|
+ * 添加银行卡
|
|
|
|
|
+ * @param bankCardData 银行卡数据
|
|
|
|
|
+ * @returns 添加的银行卡索引
|
|
|
|
|
+ */
|
|
|
|
|
+async addBankCard(bankCardData: {
|
|
|
|
|
+ bankName: string;
|
|
|
|
|
+ cardNumber: string;
|
|
|
|
|
+ cardHolder: string;
|
|
|
|
|
+ isDefault?: boolean;
|
|
|
|
|
+}): Promise<number> {
|
|
|
|
|
+ // 1. 点击"添加银行卡"按钮
|
|
|
|
|
+ const addButton = this.page.locator('[data-testid="add-bank-card-button"]');
|
|
|
|
|
+ await addButton.click();
|
|
|
|
|
+ await this.page.waitForTimeout(TIMEOUTS.SHORT);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 填写银行卡信息
|
|
|
|
|
+ // ...
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 返回新添加的银行卡索引
|
|
|
|
|
+ const cardCount = await this.getBankCardCount();
|
|
|
|
|
+ return cardCount - 1;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**读取类方法模板:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+/**
|
|
|
|
|
+ * 获取银行卡列表
|
|
|
|
|
+ * @returns 银行卡信息数组
|
|
|
|
|
+ */
|
|
|
|
|
+async getBankCardList(): Promise<Array<{
|
|
|
|
|
+ bankName: string;
|
|
|
|
|
+ cardNumber: string;
|
|
|
|
|
+ cardHolder: string;
|
|
|
|
|
+}>> {
|
|
|
|
|
+ const cards: Array<{ bankName: string; cardNumber: string; cardHolder: string }> = [];
|
|
|
|
|
+ const cardElements = this.page.locator('[data-testid^="bankcard-item-"]');
|
|
|
|
|
+
|
|
|
|
|
+ const count = await cardElements.count();
|
|
|
|
|
+ for (let i = 0; i < count; i++) {
|
|
|
|
|
+ const bankName = await cardElements.nth(i).locator('[data-testid="bank-name"]').textContent();
|
|
|
|
|
+ const cardNumber = await cardElements.nth(i).locator('[data-testid="card-number"]').textContent();
|
|
|
|
|
+ const cardHolder = await cardElements.nth(i).locator('[data-testid="card-holder"]').textContent();
|
|
|
|
|
+ cards.push({ bankName: bankName || '', cardNumber: cardNumber || '', cardHolder: cardHolder || '' });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return cards;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3.3 选择器策略优先级
|
|
|
|
|
+
|
|
|
|
|
+**优先级顺序:**
|
|
|
|
|
+1. `data-testid` - 最稳定,推荐
|
|
|
|
|
+2. `aria-label` + `role` - 语义化选择器
|
|
|
|
|
+3. `:text-is()` - 精确文本匹配
|
|
|
|
|
+4. `:has-text()` - 模糊文本匹配(尽量避免)
|
|
|
|
|
+
|
|
|
|
|
+**示例:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ✅ 优先使用 data-testid
|
|
|
|
|
+const button = this.page.locator('[data-testid="add-bank-card-button"]');
|
|
|
|
|
+
|
|
|
|
|
+// ✅ 其次使用 aria-label + role
|
|
|
|
|
+const select = this.page.getByRole('combobox', { name: '银行类型' });
|
|
|
|
|
+
|
|
|
|
|
+// ⚠️ 尽量避免模糊文本匹配
|
|
|
|
|
+const button = this.page.getByText('添加'); // 可能匹配多个元素
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3.4 超时常量定义
|
|
|
|
|
+
|
|
|
|
|
+**定义统一超时常量:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 在 Page Object 文件顶部定义
|
|
|
|
|
+const TIMEOUTS = {
|
|
|
|
|
+ IMMEDIATE: 0,
|
|
|
|
|
+ SHORT: 100, // 100ms - UI 动画
|
|
|
|
|
+ MEDIUM: 500, // 500ms - 表单提交
|
|
|
|
|
+ LONG: 1000, // 1s - 数据刷新
|
|
|
|
|
+ EXTRA_LONG: 3000, // 3s - 复杂数据持久化
|
|
|
|
|
+ NETWORK: 10000, // 10s - 网络请求
|
|
|
|
|
+} as const;
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**使用方式:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+await this.page.waitForTimeout(TIMEOUTS.SHORT); // ✅ 使用常量
|
|
|
|
|
+await this.page.waitForTimeout(500); // ❌ 硬编码
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 4. 代码审查常见问题检查清单
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 HIGH 优先级问题
|
|
|
|
|
+
|
|
|
|
|
+| 问题类型 | 检查方法 | 修复方式 |
|
|
|
|
|
+|---------|---------|---------|
|
|
|
|
|
+| Page Object 方法虚假完成 | 检查每个方法是否有实际实现 | 实现缺失的方法体 |
|
|
|
|
|
+| 路径遍历漏洞 | 检查文件路径拼接 | 使用 `path.join()` |
|
|
|
|
|
+| 数据隔离问题 | 检查全局变量 | 使用 describe 级别变量 |
|
|
|
|
|
+| 表单验证错误 | 检查验证逻辑完整性 | 添加缺失的验证 |
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 MEDIUM 优先级问题
|
|
|
|
|
+
|
|
|
|
|
+| 问题类型 | 检查方法 | 修复方式 |
|
|
|
|
|
+|---------|---------|---------|
|
|
|
|
|
+| console.log 使用 | 搜索 `console.log` | 改为 `console.debug` |
|
|
|
|
|
+| 魔法数字 | 搜索数字字面量 | 使用 TIMEOUTS 常量 |
|
|
|
|
|
+| 测试覆盖不完整 | 检查验收标准 | 添加缺失的测试 |
|
|
|
|
|
+| 注释过时 | 检查注释与代码一致性 | 更新注释 |
|
|
|
|
|
+
|
|
|
|
|
+### 4.3 代码审查前自查清单
|
|
|
|
|
+
|
|
|
|
|
+**开发者自查(提交前):**
|
|
|
|
|
+- [ ] 所有 `console.log` 已改为 `console.debug`
|
|
|
|
|
+- [ ] 所有超时值使用 TIMEOUTS 常量
|
|
|
|
|
+- [ ] Page Object 方法完整实现(非虚假完成)
|
|
|
|
|
+- [ ] 数据使用 TEST_TIMESTAMP 前缀
|
|
|
|
|
+- [ ] 移除 `test.describe.serial`(除非确实需要串行)
|
|
|
|
|
+- [ ] 测试文件无 `any` 类型
|
|
|
|
|
+- [ ] 文件路径使用 `path.join()` 或常量
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 5. 稳定性测试模式
|
|
|
|
|
+
|
|
|
|
|
+### 5.1 稳定性测试脚本
|
|
|
|
|
+
|
|
|
|
|
+**脚本模板:**
|
|
|
|
|
+```bash
|
|
|
|
|
+#!/bin/bash
|
|
|
|
|
+# Epic 稳定性测试脚本
|
|
|
|
|
+
|
|
|
|
|
+RUNS=10
|
|
|
|
|
+PASSED=0
|
|
|
|
|
+FAILED=0
|
|
|
|
|
+TIMES=()
|
|
|
|
|
+
|
|
|
|
|
+for i in $(seq 1 ${RUNS}); do
|
|
|
|
|
+ echo "=== 运行 #${i}/${RUNS} ==="
|
|
|
|
|
+ START=$(date +%s)
|
|
|
|
|
+
|
|
|
|
|
+ if pnpm test:e2e:chromium --workers=4; then
|
|
|
|
|
+ PASSED=$((PASSED + 1))
|
|
|
|
|
+ echo "✅ 运行 #${i} 通过"
|
|
|
|
|
+ else
|
|
|
|
|
+ FAILED=$((FAILED + 1))
|
|
|
|
|
+ echo "❌ 运行 #${i} 失败"
|
|
|
|
|
+ fi
|
|
|
|
|
+
|
|
|
|
|
+ END=$(date +%s)
|
|
|
|
|
+ DURATION=$((END - START))
|
|
|
|
|
+ TIMES+=($DURATION)
|
|
|
|
|
+ echo "⏱️ 耗时: ${DURATION}s"
|
|
|
|
|
+ echo ""
|
|
|
|
|
+done
|
|
|
|
|
+
|
|
|
|
|
+# 计算统计数据
|
|
|
|
|
+TOTAL=$((PASSED + FAILED))
|
|
|
|
|
+PASS_RATE=$((PASSED * 100 / TOTAL))
|
|
|
|
|
+AVG_TIME=$(awk '{sum+=$1} END {print sum/NR}' <<< "${TIMES[@]}")
|
|
|
|
|
+
|
|
|
|
|
+echo "========================================="
|
|
|
|
|
+echo "稳定性测试结果"
|
|
|
|
|
+echo "========================================="
|
|
|
|
|
+echo "通过: ${PASSED}/${TOTAL} (${PASS_RATE}%)"
|
|
|
|
|
+echo "平均时间: ${AVG_TIME}s"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 5.2 数据持久化重试机制
|
|
|
|
|
+
|
|
|
|
|
+**问题:** 表单提交后立即查询可能找不到数据
|
|
|
|
|
+
|
|
|
|
|
+**解决方案:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+/**
|
|
|
|
|
+ * 等待人员记录出现(带重试机制)
|
|
|
|
|
+ */
|
|
|
|
|
+async waitForPersonExists(name: string, options?: { timeout?: number }): Promise<boolean> {
|
|
|
|
|
+ const timeout = options?.timeout ?? 10000;
|
|
|
|
|
+ const startTime = Date.now();
|
|
|
|
|
+
|
|
|
|
|
+ while (Date.now() - startTime < timeout) {
|
|
|
|
|
+ await this.searchByName(name);
|
|
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
+
|
|
|
|
|
+ const exists = await this.personExists(name);
|
|
|
|
|
+ if (exists) {
|
|
|
|
|
+ console.debug(` ✓ 记录已出现: ${name}`);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.debug(` ✗ 记录未出现: ${name} (超时 ${timeout}ms)`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 5.3 稳定性测试迭代修复模式
|
|
|
|
|
+
|
|
|
|
|
+**Epic 9 经验:** 4 轮修复将通过率从 77.4% 提升到 90.3%
|
|
|
|
|
+
|
|
|
|
|
+| 轮次 | 通过率 | 主要修复 |
|
|
|
|
|
+|------|--------|---------|
|
|
|
|
|
+| 第 1 次 | 77.4% | 初始运行,识别问题 |
|
|
|
|
|
+| 第 2 次 | 85% | 增加表单提交等待到 3 秒 |
|
|
|
|
|
+| 第 3 次 | 91.2% | 数据持久化重试机制 |
|
|
|
|
|
+| 第 4 次 | 90.3% | 银行卡类型名称修复 |
|
|
|
|
|
+
|
|
|
|
|
+**经验总结:**
|
|
|
|
|
+- 稳定性问题通常需要多轮修复
|
|
|
|
|
+- 每轮修复应专注于一个主要问题类型
|
|
|
|
|
+- 100% 稳定性可能需要很长时间,核心功能稳定即可
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 6. 测试文件组织结构
|
|
|
|
|
+
|
|
|
|
|
+### 6.1 标准目录结构
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+web/tests/e2e/
|
|
|
|
|
+├── fixtures/ # 测试文件(图片、文档)
|
|
|
|
|
+│ └── images/
|
|
|
|
|
+│ ├── id-card-front.jpg
|
|
|
|
|
+│ ├── id-card-back.jpg
|
|
|
|
|
+│ └── disability-card.jpg
|
|
|
|
|
+├── pages/ # Page Object
|
|
|
|
|
+│ └── admin/
|
|
|
|
|
+│ ├── disability-person.page.ts
|
|
|
|
|
+│ ├── region-management.page.ts
|
|
|
|
|
+│ └── order-management.page.ts
|
|
|
|
|
+├── specs/ # 测试用例
|
|
|
|
|
+│ └── admin/
|
|
|
|
|
+│ ├── disability-person-photo.spec.ts
|
|
|
|
|
+│ ├── disability-person-bankcard.spec.ts
|
|
|
|
|
+│ ├── disability-person-note.spec.ts
|
|
|
|
|
+│ ├── disability-person-visit.spec.ts
|
|
|
|
|
+│ └── disability-person-crud.spec.ts
|
|
|
|
|
+├── scripts/ # 测试脚本
|
|
|
|
|
+│ └── run-stability-test.sh
|
|
|
|
|
+└── playwright.config.ts # Playwright 配置
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 6.2 测试文件命名规范
|
|
|
|
|
+
|
|
|
|
|
+| 文件类型 | 命名格式 | 示例 |
|
|
|
|
|
+|---------|---------|------|
|
|
|
|
|
+| 测试用例 | `{feature}-{action}.spec.ts` | `disability-person-crud.spec.ts` |
|
|
|
|
|
+| Page Object | `{entity}.page.ts` | `disability-person.page.ts` |
|
|
|
|
|
+| 测试脚本 | `run-{test-type}.sh` | `run-stability-test.sh` |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 应用示例:Epic 10 订单管理测试
|
|
|
|
|
+
|
|
|
|
|
+### 应用 Epic 9 模式
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// Epic 10: 订单管理测试 - 应用 Epic 9 模式
|
|
|
|
|
+
|
|
|
|
|
+test.describe('订单管理 - 创建订单', () => {
|
|
|
|
|
+ // 模式 1: 时间戳前缀
|
|
|
|
|
+ const TEST_TIMESTAMP = Date.now();
|
|
|
|
|
+ const TEST_PREFIX = `test_order_${TEST_TIMESTAMP}`;
|
|
|
|
|
+ const createdOrders: Array<{ orderNo: string }> = [];
|
|
|
|
|
+
|
|
|
|
|
+ test('应该成功创建订单', async ({ orderManagementPage }) => {
|
|
|
|
|
+ const orderData = {
|
|
|
|
|
+ orderNo: `${TEST_PREFIX}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
+ // ...
|
|
|
|
|
+ };
|
|
|
|
|
+ await orderManagementPage.createOrder(orderData);
|
|
|
|
|
+ createdOrders.push(orderData);
|
|
|
|
|
+
|
|
|
|
|
+ // 模式 2: 数据持久化重试
|
|
|
|
|
+ const exists = await orderManagementPage.waitForOrderExists(orderData.orderNo, { timeout: 15000 });
|
|
|
|
|
+ expect(exists).toBe(true);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test.afterEach(async ({ orderManagementPage }) => {
|
|
|
|
|
+ // 模式 3: 清理钩子
|
|
|
|
|
+ for (const order of createdOrders) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await orderManagementPage.deleteOrder(order.orderNo);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.debug(` ⚠ 清理订单失败: ${order.orderNo}`, error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 总结
|
|
|
|
|
+
|
|
|
|
|
+Epic 9 建立的可复用模式:
|
|
|
|
|
+
|
|
|
|
|
+| 模式 | 关键要点 | 受益者 |
|
|
|
|
|
+|------|---------|--------|
|
|
|
|
|
+| 数据隔离 | TEST_TIMESTAMP + TEST_PREFIX | 所有 Epic |
|
|
|
|
|
+| 并行执行 | 移除 serial,使用 workers | 所有 Epic |
|
|
|
|
|
+| Page Object | 动词+名词命名,完整实现 | 所有 Epic |
|
|
|
|
|
+| 超时常量 | TIMEOUTS 常量,避免魔法数字 | 所有 Epic |
|
|
|
|
|
+| 稳定性测试 | 多轮迭代,渐进式修复 | 所有 Epic |
|
|
|
|
|
+
|
|
|
|
|
+**Epic 9 是测试模式的黄金标准。** 后续 Epic 应该参考这些模式,确保测试的一致性和可维护性。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**文档维护:** 当发现新的模式或改进时,请更新本文档。
|