Files
hms/docs/superpowers/specs/2026-04-28-e2e-testing-design.md
iven 4eb874f52d docs(e2e): 添加 E2E 测试设计规格文档
流程链路式双端 E2E 测试体系设计:
- Web 端 5 条业务链路(Playwright + Page Object)
- 小程序端 4 条业务链路(Vitest + miniprogram-automator)
- API 驱动自建自毁数据策略,乐观锁 version 支持
- CI-ready 环境变量驱动设计
2026-04-28 21:57:19 +08:00

24 KiB
Raw Permalink Blame History

E2E 测试设计规格 — HMS 健康管理平台

日期: 2026-04-28 | 状态: 设计中

Context

HMS 健康管理平台当前 E2E 测试仅有 10 个基础用例(登录/用户/插件/租户隔离),健康模块 ~27 个 Web 页面路由和微信小程序端完全无 E2E 覆盖。wiki 已将此标注为 P2 缺口。本设计旨在建立双端Web + 小程序)的流程链路式 E2E 测试体系。

大纲

  1. 整体架构与目录结构
  2. 关键业务链路定义
  3. 测试数据策略与 API Client
  4. Page Object 与 MCP Helper 设计
  5. 执行策略与 CI-Ready 设计

§1 整体架构与目录结构

设计决策

维度 选择 原因
覆盖范围 Web 全量 + 小程序并行 双端同步覆盖,最大化业务保障
测试深度 关键流程冒烟测试 务实覆盖最重要的用户路径
小程序实现 WeChat MCP 自动化 利用已有 MCP 联调能力
测试架构 双端独立框架 Web 用 Playwright小程序用 Vitest + MCP
测试数据 API 驱动自建自毁 每个测试独立创建/清理数据
CI 策略 本地优先CI-ready 设计 环境变量驱动,未来可接入 GitHub Actions

目录结构

apps/web/e2e/                          # Web 端 E2EPlaywright
├── flows/                             # 业务链路测试
│   ├── patient-journey.spec.ts        # 患者全流程
│   ├── vital-signs-flow.spec.ts       # 体征数据链路
│   ├── appointment-flow.spec.ts       # 预约排班链路
│   ├── follow-up-flow.spec.ts         # 随访管理链路
│   └── alert-flow.spec.ts             # 告警处理链路
├── smoke/                             # 基础冒烟(迁移现有 4 个 spec
│   ├── login.spec.ts
│   ├── users.spec.ts
│   ├── plugins.spec.ts
│   └── tenant-isolation.spec.ts
├── fixtures/                          # 测试基础设施
│   ├── auth.fixture.ts                # 认证 fixture增强现有
│   ├── api-client.ts                  # API 数据准备/清理客户端
│   └── test-data.ts                   # 测试数据工厂函数
├── pages/                             # 轻量 Page Object仅关键页面
│   ├── login.page.ts
│   ├── patient-list.page.ts
│   ├── patient-detail.page.ts
│   ├── appointment.page.ts
│   └── health-data.page.ts
└── playwright.config.ts               # 增强配置

apps/miniprogram/e2e/                  # 小程序端 E2EMCP 自动化)
├── flows/                             # 业务链路测试
│   ├── patient-health-view.spec.ts    # 患者查看健康数据
│   ├── vital-signs-input.spec.ts      # 体征录入
│   ├── points-flow.spec.ts            # 积分签到兑换
│   └── mall-flow.spec.ts             # 积分商城
├── helpers/                           # MCP 封装
│   ├── mcp-client.ts                  # MCP 工具调用封装
│   ├── auth.helper.ts                 # 小程序登录/Token 注入
│   └── navigation.helper.ts           # 页面导航封装
├── check-readiness.ts                 # 环境就绪检查
└── vitest.config.ts                   # 测试运行器配置

关键原则

  1. 现有 4 个 smoke test 原地迁移到 smoke/ 目录,逻辑不变
  2. Web 端保持 Playwright小程序端用 Vitest + miniprogram-automator,各自独立运行
  3. 两端 API client 逻辑保持一致但代码各自维护Web 运行在 Node小程序也运行在 Node可考虑后续抽取共享包
  4. Page Object 只封装关键页面,随访/告警等简单页面直接在 spec 中操作
  5. 每条链路自包含API 准备所需数据),不依赖其他链路的执行顺序
  6. 所有 Page Object 使用 Hash Router 路径格式(/#/health/patients

§2 关键业务链路定义

Web 端链路5 条)

Flow 1: 患者全流程 patient-journey.spec.ts

登录 → 患者列表页 → 创建患者 → 查看患者详情 → 编辑患者信息
→ 分配医生 → 添加家族成员 → 删除测试数据

验证点:患者 CRUD、详情页数据完整性、医生关联、家族成员管理

Flow 2: 体征数据链路 vital-signs-flow.spec.ts

登录 → 选择患者 → 录入体征数据(血压/心率/血糖)
→ 查看体征列表 → 查看趋势图 → 录入化验报告 → 审核报告
→ 清理数据

验证点:体征录入表单、列表展示、趋势图表渲染、化验报告上传与审核

Flow 3: 预约排班链路 appointment-flow.spec.ts

登录 → 创建医生 → 设置排班 → 患者列表 → 创建预约
→ 查看预约列表 → 更新预约状态 → 清理数据

验证点:排班日历交互、预约创建、状态流转(待确认→已确认→已完成)

Flow 4: 随访管理链路 follow-up-flow.spec.ts

登录 → 创建随访模板 → 批量创建随访任务 → 查看任务列表
→ 完成随访(填写记录)→ 查看随访记录 → 清理数据

验证点:模板管理、批量操作、任务分配、随访记录填写

Flow 5: 告警处理链路 alert-flow.spec.ts

登录(API) → 创建告警规则(低阈值,如心率 > 50
→ 录入触发体征数据API 创建心率为 110 的体征)→ 轮询等待告警生成(最多 10s
→ 查看告警列表UI→ 确认告警 → 处理告警 → 查看告警规则
→ 停用规则 → 清理数据(规则 + 体征 + 告警)

验证点:告警规则配置、告警自动生成、列表/仪表盘展示、状态流转(待处理→已确认→已处理) 异步处理说明: 告警由后台任务根据规则匹配体征数据自动生成。测试通过 API 创建低阈值规则 + 触发数据,然后轮询告警列表 API 等待告警出现(await expect.poll(() => api.getAlerts(), { timeout: 10_000 }))。

小程序端链路4 条)

Flow 6: 患者健康数据查看 patient-health-view.spec.ts

MCP 登录 → 首页 → 查看今日体征摘要 → 查看体征趋势
→ 查看随访任务列表 → 查看化验报告

Flow 7: 体征数据录入 vital-signs-input.spec.ts

MCP 登录 → 进入录入页 → 填写血压/心率 → 提交
→ 验证数据出现在列表 → 清理数据

Flow 8: 积分签到兑换 points-flow.spec.ts

MCP 登录 → 每日签到 → 查看积分余额 → 浏览商品
→ 兑换商品 → 查看订单列表

Flow 9: 积分商城 mall-flow.spec.ts

MCP 登录 → 商城首页 → 浏览商品分类 → 查看商品详情
→ 查看线下活动 → 活动报名

链路覆盖矩阵

业务模块 Web 链路 小程序链路 API 链路(已有)
患者管理 Flow 1 Chain 1
体征/健康数据 Flow 2 Flow 6, 7 Chain 1, 2
预约排班 Flow 3 Chain 1
随访管理 Flow 4 Flow 6 Chain 1
告警系统 Flow 5
积分体系 Flow 8 Chain 4
积分商城 Flow 9 Chain 4

未覆盖模块说明:

  • 咨询管理2 页面)— 有完整 API 链路,但实时消息交互难以用 E2E 验证,建议 API 集成测试覆盖
  • 内容/文章管理4 页面)— CRUD 操作较标准,优先级低于核心医疗流程
  • AI 分析3 页面)— 依赖外部 AI 服务,不稳定,建议 mock 测试
  • 统计仪表盘5 页面)— 数据聚合展示E2E 价值有限,建议视觉回归测试

§3 测试数据策略与 API Client

核心原则

API 驱动自建自毁:每个测试通过 API 创建所需数据,测试结束后清理。不依赖数据库预置状态。

API Client

后端所有实体使用乐观锁(version 字段),更新/删除操作必须携带 version。API Client 的所有方法返回完整实体(含 version清理操作使用返回的 version。

// fixtures/api-client.ts

interface VersionedEntity { id: string; version: number }

class ApiClient {
  private token: string;
  private baseUrl: string;

  // 认证
  async login(username: string, password: string): Promise<AuthResponse>
  async loginAsAdmin(): Promise<AuthResponse>

  // 患者数据工厂 — 返回含 version 的完整实体
  async createPatient(overrides?: Partial<PatientData>): Promise<Patient & VersionedEntity>
  async updatePatient(id: string, version: number, data: Partial<PatientData>): Promise<Patient & VersionedEntity>
  async deletePatient(id: string, version: number): Promise<void>

  // 医生数据工厂
  async createDoctor(overrides?: Partial<DoctorData>): Promise<Doctor & VersionedEntity>
  async deleteDoctor(id: string, version: number): Promise<void>

  // 体征数据工厂
  async createVitalSigns(patientId: string, overrides?: Partial<VitalSignsData>): Promise<VitalSigns & VersionedEntity>
  async deleteVitalSigns(patientId: string, id: string, version: number): Promise<void>

  // 预约数据工厂
  async createAppointment(overrides?: Partial<AppointmentData>): Promise<Appointment & VersionedEntity>
  async updateAppointmentStatus(id: string, version: number, status: string): Promise<Appointment & VersionedEntity>
  async deleteAppointment(id: string, version: number): Promise<void>

  // 随访数据工厂
  async createFollowUpTemplate(overrides?: Partial<TemplateData>): Promise<Template & VersionedEntity>
  async createFollowUpTask(overrides?: Partial<TaskData>): Promise<Task & VersionedEntity>
  async deleteFollowUpTemplate(id: string, version: number): Promise<void>
  async deleteFollowUpTask(id: string, version: number): Promise<void>

  // 告警数据工厂
  async createAlertRule(overrides?: Partial<AlertRuleData>): Promise<AlertRule & VersionedEntity>
  async deleteAlertRule(id: string, version: number): Promise<void>
  // 告警状态流转(需要 version
  async acknowledgeAlert(id: string, version: number): Promise<Alert & VersionedEntity>
  async resolveAlert(id: string, version: number): Promise<Alert & VersionedEntity>
  async dismissAlert(id: string, version: number): Promise<Alert & VersionedEntity>

  // 排班数据工厂
  async createSchedule(overrides?: Partial<ScheduleData>): Promise<Schedule & VersionedEntity>
  async deleteSchedule(id: string, version: number): Promise<void>

  // 通用 HTTP
  async get<T>(path: string): Promise<T>
  async post<T>(path: string, body: unknown): Promise<T>
  async put<T>(path: string, body: unknown): Promise<T>
  async delete(path: string, body?: unknown): Promise<void>
}

测试数据工厂

// fixtures/test-data.ts

function makePatient(overrides?: Partial<PatientData>): PatientData {
  const id = `test_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
  return {
    name: `测试患者_${id}`,
    gender: 'male',
    birthDate: '1990-01-15',
    phone: `138${Math.random().toString().slice(2, 11)}`,
    idNumber: `110101199001${Math.random().toString().slice(2, 8)}`,
    ...overrides,
  };
}

function makeDoctor(overrides?: Partial<DoctorData>): DoctorData { ... }
function makeVitalSigns(overrides?: Partial<VitalSignsData>): VitalSignsData { ... }
function makeAppointment(patientId: string, doctorId: string, scheduleId: string): AppointmentData { ... }

每次生成唯一数据(时间戳 + 随机串),避免测试间冲突。

数据生命周期管理

所有删除/更新操作必须携带 version乐观锁。创建操作的返回值包含 version清理时使用。

Web 端Playwright— try/finally 模式:

test('患者全流程', async ({ page, api }) => {
  const cleanup: Array<() => Promise<void>> = [];
  try {
    const patient = await api.createPatient();
    // 创建时注册清理函数,保存 id + version
    cleanup.push(() => api.deletePatient(patient.id, patient.version));

    const doctor = await api.createDoctor();
    cleanup.push(() => api.deleteDoctor(doctor.id, doctor.version));

    // ... 测试操作(更新时使用最新 version...
  } finally {
    // 逆序清理(后创建的先删除,避免外键约束问题)
    for (const fn of cleanup.reverse()) {
      await fn().catch(() => {});  // 忽略已删除的错误
    }
  }
});

小程序端Vitest— afterEach 模式:

describe('体征数据录入', () => {
  const cleanup: Array<() => Promise<void>> = [];

  afterEach(async () => {
    for (const fn of cleanup.reverse()) await fn().catch(() => {});
    cleanup.length = 0;
  });

  it('录入血压数据', async () => {
    const vitalSigns = await api.createVitalSigns(patientId);
    cleanup.push(() => api.deleteVitalSigns(patientId, vitalSigns.id, vitalSigns.version));
    // ... 断言 ...
  });
});

Fixture 组合Web Playwright 将认证 page 和 ApiClient 注入到同一个 test fixture

// fixtures/auth.fixture.ts — 增强版
import { test as base } from '@playwright/test';
import { ApiClient } from './api-client';

type E2eFixtures = {
  api: ApiClient;
  authenticatedPage: Page;
};

export const test = base.extend<E2eFixtures>({
  // 注入已认证的 ApiClient
  api: async ({}, use) => {
    const client = new ApiClient();
    await client.loginAsAdmin();
    await use(client);
  },
  // 注入已认证的 page继承现有 auth.fixture 逻辑)
  authenticatedPage: async ({ page }, use) => {
    const client = new ApiClient();
    const { access_token, refresh_token, user } = await client.loginAsAdmin();
    await page.addInitScript(({ token, refresh, userData }) => {
      localStorage.setItem('access_token', token);
      localStorage.setItem('refresh_token', refresh);
      localStorage.setItem('user', userData);
    }, { token: access_token, refresh: refresh_token, userData: JSON.stringify(user) });
    await use(page);
  },
});

认证策略

认证方式 账号
Webadmin API login → localStorage 注入 admin / Admin@2026
Web受限角色 同上,不同账号 用于权限验证场景
小程序 MCP ensureConnection → storage 注入 token mp_e2e_test / Test@2026

凭证从环境变量读取fallback 到开发默认值:

const TEST_ADMIN = {
  username: process.env.E2E_ADMIN_USER || 'admin',
  password: process.env.E2E_ADMIN_PASS || 'Admin@2026',
};

§4 Page Object 与 MCP Helper 设计

Web 端 Page Object5 个,轻量级)

每个 Page Object 包含 3 部分:导航操作断言辅助。只封装关键页面,随访/告警等简单页面直接在 spec 中操作。

Login.page.ts

class LoginPage {
  async goto(): Promise<void>
  async login(username: string, password: string): Promise<void>
  async getErrorMessage(): Promise<string>
  async isLoggedIn(): Promise<boolean>
}

PatientList.page.ts

class PatientListPage {
  async goto(): Promise<void>                          // 导航 + 等待表格加载
  async clickCreate(): Promise<void>
  async fillCreateForm(data: PatientData): Promise<void>
  async submitCreateForm(): Promise<void>
  async searchPatient(name: string): Promise<void>
  async clickPatientRow(index: number): Promise<void>
  async getTableRowCount(): Promise<number>
  async hasPatientInTable(name: string): Promise<boolean>
}

PatientDetail.page.ts

class PatientDetailPage {
  async goto(id: string): Promise<void>
  async getPatientName(): Promise<string>
  async clickTab(tabName: string): Promise<void>       // 体征/随访/化验/诊断
  async getVitalSignsCount(): Promise<number>
  async assignDoctor(doctorName: string): Promise<void>
  async addFamilyMember(data: FamilyMemberData): Promise<void>
}

HealthData.page.ts

class HealthDataPage {
  async clickAddVitalSigns(): Promise<void>
  async fillVitalSignsForm(data: VitalSignsData): Promise<void>
  async submitVitalSigns(): Promise<void>
  async getVitalSignsList(): Promise<VitalSignsData[]>
  async trendChartIsVisible(): Promise<boolean>
  async uploadLabReport(filePath: string): Promise<void>
  async reviewLabReport(reportId: string): Promise<void>
}

Appointment.page.ts

class AppointmentPage {
  async gotoSchedule(): Promise<void>
  async gotoAppointments(): Promise<void>
  async selectDateOnCalendar(date: string): Promise<void>
  async createScheduleSlot(data: ScheduleData): Promise<void>
  async createAppointment(data: AppointmentData): Promise<void>
  async updateStatus(appointmentId: string, status: string): Promise<void>
}

小程序端 Automator Helper3 个)

小程序使用 miniprogram-automator npm 包(项目已有依赖,见 apps/miniprogram/test-automator.mjs 通过 WeChat DevTools 的自动化接口操作小程序。与 MCP 工具相同的底层能力,但可独立运行在 Vitest 中。

前置条件: 需要先构建小程序(pnpm build:weappWeChat DevTools 需打开项目并启用自动化端口。

AutomatorClient 封装 miniprogram-automator 为 TypeScript API

import automator from 'miniprogram-automator';

class AutomatorClient {
  private mini: automator.MiniProgram;

  // 连接/启动小程序
  async connect(cliPath?: string, projectPath?: string): Promise<void>
  async disconnect(): Promise<void>

  // 页面操作
  async currentPage(): Promise<automator.Page>
  async navigateTo(path: string, query?: Record<string, string>): Promise<void>
  async navigateBack(): Promise<void>
  async reLaunch(path: string): Promise<void>

  // 元素操作
  async waitForElement(selector: string, timeout?: number): Promise<automator.Element>
  async tap(selector: string): Promise<void>
  async inputText(selector: string, value: string): Promise<void>
  async getElement(selector: string): Promise<automator.Element>
  async getElements(selector: string): Promise<automator.Element[]>

  // 页面数据
  async getPageData(path?: string): Promise<any>
  async screenshot(path?: string): Promise<Buffer>
}

MpAuthHelper 小程序认证

class MpAuthHelper {
  constructor(private client: AutomatorClient, private api: ApiClient) {}
  // API 获取 token → 通过 automator 写入 storage → reLaunch 刷新
  async loginAsTestPatient(): Promise<void>
}

MpNavigator 小程序页面导航封装

class MpNavigator {
  constructor(private client: AutomatorClient) {}
  async goToHealthHome(): Promise<void>           // reLaunch /pages/pkg-health/index
  async goToVitalSignsInput(): Promise<void>      // navigateTo 输入页
  async goToVitalSignsTrend(): Promise<void>      // navigateTo 趋势页
  async goToPointsCheckin(): Promise<void>        // navigateTo 签到页
  async goToMall(): Promise<void>                 // reLaunch 商城首页
  async goToFollowUpTasks(): Promise<void>        // navigateTo 随访列表
  async goToOrders(): Promise<void>               // navigateTo 订单列表
}

小程序页面路径参考(来自 app.config.ts

  • 患者首页: /pages/pkg-health/index
  • 体征录入: /pages/pkg-health/input/index
  • 体征趋势: /pages/pkg-health/trend/index
  • 积分签到: /pages/pkg-profile/index(签到入口在个人中心)
  • 积分商城: /pages/pkg-mall/index
  • 随访任务: /pages/pkg-health/followups/index
  • 订单列表: /pages/pkg-mall/orders/index

层级关系

┌─────────────────────────────────────────┐
│              Flow Spec                   │  ← 业务链路测试用例
├──────────────┬──────────────────────────┤
│  Page Object │  MCP Helper              │  ← UI 操作抽象
│  (Web 端)    │  (小程序端)               │
├──────────────┴──────────────────────────┤
│          API Client                      │  ← 数据准备/清理
├─────────────────────────────────────────┤
│     Test Data Factory                    │  ← 随机数据生成
├─────────────────────────────────────────┤
│     Auth Fixture / Auth Helper           │  ← 认证
└─────────────────────────────────────────┘

§5 执行策略与 CI-Ready 设计

本地执行命令

# Web 端 E2E
cd apps/web
pnpm test:e2e                          # 运行全部smoke + flows
pnpm test:e2e -- --grep @smoke         # 只跑冒烟
pnpm test:e2e -- --grep "患者全流程"    # 指定链路
pnpm test:e2e:ui                       # 交互式 UI 模式

# 小程序端 E2E
cd apps/miniprogram
pnpm test:e2e                          # 运行全部
pnpm test:e2e -- --reporter=verbose    # 详细输出

Playwright Config 增强

关键变更:

  • testDir: './e2e'保持不变smoke/ 和 flows/ 子目录都在 e2e 下)
  • testMatch: ['smoke/**/*.spec.ts', 'flows/**/*.spec.ts']
  • timeout: 60_000(链路测试需要更长时间)
  • fullyParallel: false(链路间有数据依赖,串行更稳定)
  • reporter: [['html'], ['list']]
  • globalSetup: './e2e/check-readiness'
  • 标签过滤:@smoke 快速验证,@flow 完整链路

小程序 Vitest Config

新增依赖: apps/miniprogram/package.json 需添加:

  • vitest(测试运行器)
  • test:e2e script
  • miniprogram-automator 已在项目中(test-automator.mjs 使用)

配置:

  • testTimeout: 30_000
  • testSequence: 'sequential'automator 操作必须串行)
  • globalSetup: check-readiness
  • 测试前需执行 pnpm build:weapp 构建小程序
  • WeChat DevTools 必须打开项目并启用自动化端口

前置条件检查

测试运行前自动验证环境就绪。Web 端检查 http://localhost:3000/health/live + http://localhost:5174。小程序端额外检查 WeChat DevTools 自动化端口可达。最多重试 5 次,间隔 2s任一失败则打印明确提示并退出。

CI-Ready 设计

本地优先,但设计上保证 CI 兼容:

环境变量驱动:

E2E_BASE_URL=http://localhost:5174
E2E_API_URL=http://localhost:3000
E2E_ADMIN_USER=admin
E2E_ADMIN_PASS=Admin@2026
E2E_MP_USER=mp_e2e_test
E2E_MP_PASS=Test@2026
E2E_HEADLESS=true
E2E_SKIP_MP=false              # CI 中可跳过小程序测试

GitHub Actions 预留:

# .github/workflows/e2e.yml — 未来启用
name: E2E Tests
jobs:
  web-e2e:
    services:
      postgres: { ... }
    steps:
      - run: cargo run -p erp-server &
      - run: cd apps/web && pnpm test:e2e

测试结果输出:

  • Playwright HTML 报告 → apps/web/e2e-results/
  • Vitest JUnit 报告 → apps/miniprogram/e2e-results/

执行耗时预估

场景 命令 预计耗时
Web 全量 pnpm test:e2e ~3-5 分钟
Web 冒烟 --grep @smoke ~30 秒
Web 单条链路 --grep "患者" ~30-60 秒
小程序全量 pnpm test:e2e ~2-4 分钟
双端全量 根目录脚本串联 ~6-10 分钟

验证方式

  1. cd apps/web && pnpm test:e2e — Web 端全部通过
  2. cd apps/miniprogram && pnpm test:e2e — 小程序端全部通过
  3. 确认测试数据已清理(数据库无残留测试记录)
  4. --grep @smoke 可在 30 秒内完成冒烟验证
  5. HTML 报告可正常打开查看