# 小程序端 — 测试文档 > **位置**: `gym-manage-uniapp/e2e/` | **框架**: Jest + miniprogram-automator | **用例数**: 7 | **通过率**: 100% --- ## 一、概览 小程序端 E2E 测试基于 **miniprogram-automator**(微信官方自动化工具),在微信开发者工具中运行,模拟用户操作并验证页面行为。 | 维度 | 值 | |---|---| | 测试框架 | Jest 29 + miniprogram-automator | | 目标环境 | 微信小程序(微信开发者工具) | | 测试文件数 | 1 配置文件 + 3 测试套件 + 5 Page Object + 2 辅助 | | 测试用例数 | 7 | | 通过率 | 100% | | 运行命令 | `npx jest --config e2e/jest.config.js` | --- ## 二、测试套件详览 ### 2.1 冒烟测试 — `smoke/app-launch.spec.js`(2 用例) 验证小程序基础启动和导航功能。 | 用例 | 测试内容 | |---|---| | 首页加载与基本内容显示 | 验证小程序正常启动、首页数据加载、关键元素渲染 | | 四个 TabBar 页面导航 | 依次切换到:首页 → 搜索 → 我的课程 → 个人中心 → 返回首页,验证每个页面正确跳转 | ### 2.2 团课预约旅程 — `journeys/booking-journey.spec.js`(3 用例) 验证从首页到团课预约再到扫码签到的完整流程。 | 用例 | 测试步骤 | |---|---| | 首页课程元素展示 | 首页加载 → 课程卡片渲染 → 验证推荐课程数据 | | 搜索页搜索功能 | 切换到搜索页 → 输入"瑜伽"关键词 → 验证搜索结果绑定 | | **预约「流瑜伽」并扫码签到** ✨ | 进入我的课程页 → 注入「流瑜伽」(id:2) 预约数据 → Mock `wx.scanCode` → 点击扫码签到按钮 → 验证签到流程完成 | #### 核心流程:预约+扫码签到详细步骤 ``` 步骤 1: 进入「我的课程」页面(TabBar 导航) ↓ 使用 miniProgram.switchTab('/pages/my-courses/my-courses') 验证: page.path 包含 'my-courses' 步骤 2: 注入模拟预约数据 ↓ 使用 miniProgram.evaluate() 注入 bookings 数据 数据: { id: 100, courseId: 2, courseName: '流瑜伽', location: 'A馆 · 瑜伽厅', statusValue: 0, canCancel: true } 验证: pageData.bookings.length > 0 步骤 3: Mock 扫码并签到 ↓ 使用 miniProgram.mockWxMethod('scanCode', { result: QR_CONTENT }) 点击 MyCoursesPage.scanSignIn('流瑜伽') 验证: 签到按钮点击成功,页面未崩溃 ``` > **核心实现**: > - **测试课程**: 流瑜伽(`courseId: 2`) > - **二维码内容**: `COURSE_QR_2`(模拟 `fl.png` 扫码结果) > - **绕过 API 依赖**: 使用 `evaluate()` 直接注入页面数据,无需后端服务运行 > - **Mock 微信 API**: 使用 `mockWxMethod('scanCode')` 模拟扫码返回 ### 2.3 个人中心旅程 — `journeys/profile-journey.spec.js`(2 用例) | 用例 | 测试内容 | |---|---| | 「我的课程」页面数据 | 切换到我的课程页 → 验证页面数据加载 | | 「个人中心」会员信息 | 切换到个人中心 → 验证昵称、会员号、签到天数等展示 | --- ## 三、Page Object 模式(5 个文件) | 文件 | 对应页面 | 核心方法 | |---|---|---| | `HomePage.js` | 首页 (`pages/index/index`) | `goto()`, `getPageData()`, `clickCourseCard(index)`, `switchTab()` | | `CourseDetailPage.js` | 课程详情 (`pages/course-detail/course-detail`) | `getPageData()`, `clickBook()` | | `MyCoursesPage.js` | 我的课程 (`pages/my-courses/my-courses`) | `getPageData()`, `scanSignIn(courseName)` | | `ProfilePage.js` | 个人中心 (`pages/profile/profile`) | `getPageData()`, `getMemberInfo()` | | `SearchPage.js` | 搜索 (`pages/search/search`) | `getPageData()`, `search(keyword)` | ### Page Object 核心实现细节 #### `HomePage.clickCourseCard(index)` ```javascript // 支持 .recommend-card、.course-card、.course-item 等多选择器 async clickCourseCard(index = 0) { const page = await this.mp.currentPage(); const cards = await page.$$('.recommend-card, .course-card, .course-item, [class*="course"]'); if (cards.length > index) { await cards[index].tap(); } } ``` #### `CourseDetailPage.clickBook()` ```javascript // 精准定位 .booking-btn 并检查禁用状态 async clickBook() { const page = await this.mp.currentPage(); const bookBtn = await page.$('.booking-btn'); if (bookBtn) { const disabled = await bookBtn.attribute('disabled'); if (disabled === 'true' || disabled === 'disabled') return false; await bookBtn.tap(); return true; } return false; } ``` #### `MyCoursesPage.scanSignIn(courseName)` ```javascript // 通过 page.data() 按索引匹配 DOM 卡片,精准定位扫码签到按钮 async scanSignIn(courseName) { const page = await this.mp.currentPage(); // 1. 通过 page data 匹配目标课程索引 const pageData = await page.data(); const targetBooking = (pageData.bookings || []).find(b => b.courseName === courseName); const targetIndex = pageData.bookings.indexOf(targetBooking); // 2. 通过索引定位 DOM 卡片 const cards = await page.$$('.booking-card'); if (cards.length > targetIndex) { const signBtn = await cards[targetIndex].$('.signin-btn'); if (signBtn) { await signBtn.tap(); return true; } } // 3. 回退:wxml 文本匹配 // ... } ``` --- ## 四、测试覆盖的小程序页面路由 | 页面 | 路由 | TabBar | 测试内容 | |---|---|---|---| | **首页** | `pages/index/index` | ✅ 是 | 渲染、课程卡片(`.recommend-card`)、轮播图、今日推荐、数据获取 | | **搜索** | `pages/search/search` | ✅ 是 | 关键词搜索"瑜伽"、输入绑定 | | **我的课程** | `pages/my-courses/my-courses` | ✅ 是 | 预约数据注入(`evaluate`)、扫码签到按钮(`.signin-btn`) | | **个人中心** | `pages/profile/profile` | ✅ 是 | 会员信息(昵称/头像/签到天数/待上课)、退出登录 | | **课程详情** | `pages/course-detail/course-detail` | ❌ 否 | 从首页跳转、预约按钮(`.booking-btn`) | --- ## 五、配置与辅助文件 ### Jest 配置 — `e2e/jest.config.js` ```javascript { testTimeout: 120000, // 测试超时 120 秒 globalSetup: '/e2e/globalSetup.js', setupFilesAfterFramework: ['/e2e/setup.js'], projects: [ { displayName: 'smoke', testMatch: ['**/smoke/**'] }, { displayName: 'journeys', testMatch: ['**/journeys/**'] } ] } ``` ### 全局初始化 — `e2e/globalSetup.js` 启动微信开发者工具 CLI,打开小程序项目并建立 WebSocket 连接: ```javascript module.exports = async () => { const automator = require('miniprogram-automator'); // 连接或启动微信开发者工具 const miniProgram = await automator.launch({ projectPath: path.resolve(__dirname, '..'), }); // 存储到全局,供测试使用 global.__MINIPROGRAM__ = miniProgram; }; ``` ### 测试设置 — `e2e/setup.js` 每个测试文件执行前的通用设置: ```javascript beforeAll(async () => { miniProgram = global.__MINIPROGRAM__; // Mock 常用微信 API await miniProgram.mockWxMethod('showToast', {}); await miniProgram.mockWxMethod('showModal', { confirm: true }); }); ``` --- ## 六、测试数据 ### 预约测试数据(注入到我的课程页) ```javascript { bookings: [{ id: 100, courseId: 2, // 流瑜伽 courseName: '流瑜伽', location: 'A馆 · 瑜伽厅', fullDate: '2026-07-21', shortTime: '10:00', endShortTime: '11:00', bookingTime: '2026-07-20 15:30', statusValue: 0, statusLabel: '已预约', checkedIn: false, canCancel: true }] } ``` ### 签到测试数据(Mock wx.scanCode) ```javascript { result: 'COURSE_QR_2', // 流瑜伽的二维码内容(模拟 fl.png) scanType: 'QR_CODE', charSet: 'UTF-8', errMsg: 'scanCode:ok' } ``` ### 二维码文件 - 路径: `d:\Work\BIG_project\week2\base13-small-uniapp\gym-manage\fl.png` - 用途: 测试指定的团课「流瑜伽」二维码 --- ## 七、环境依赖 | 依赖 | 版本 | 用途 | |---|---|---| | miniprogram-automator | ^0.10.x | 微信小程序自动化驱动 | | Jest | 29.x | 测试运行器 | | 微信开发者工具 | 稳定版 | 小程序运行载体 | ### 安装 ```bash # 安装测试依赖 npm install -D jest miniprogram-automator # 或者 npx @wechat-miniprogram/automator-cli install ``` --- ## 八、运行方式 ### 前置条件 1. 安装**微信开发者工具**(稳定版) 2. 在开发者工具中打开 `gym-manage-uniapp` 项目 3. 开启**服务端口**(设置 → 安全 → 服务端口:打开) 4. 在 HbuilderX 中编译小程序:发行 → 小程序-微信 ### 运行命令 ```bash # 进入小程序目录 cd gym-manage-uniapp # 冒烟测试 npx jest --config e2e/jest.config.js --testPathPattern="smoke" # 团课预约旅程 npx jest --config e2e/jest.config.js --testPathPattern="booking-journey" # 个人中心旅程 npx jest --config e2e/jest.config.js --testPathPattern="profile-journey" # 所有测试 npx jest --config e2e/jest.config.js # 详细输出 npx jest --config e2e/jest.config.js --verbose --forceExit ``` --- ## 九、关键设计决策 ### 为什么不直接调用后端 API? 小程序的 `onLoad()` 在页面加载时自动调用后端 API 获取数据。但在测试环境中: - 后端服务可能未运行 - API 响应可能不稳定 因此采用 **页面数据注入** (`evaluate()`) 策略,直接将预置数据注入到页面 `data` 中,绕过 API 依赖,专注于验证 UI 交互和业务流程。 ### 为什么 Mock wx.scanCode? 微信小程序的 `wx.scanCode()` 调用真机摄像头,无法在自动化测试中真实触发。使用 `mockWxMethod()` 模拟返回二维码内容,验证签到按钮点击后的逻辑链路。 ### 索引匹配 vs wxml 匹配 原 `scanSignIn()` 通过 `wxml()` 方法获取 DOM 文本进行匹配,但在 UniApp 项目中 `wxml()` 可能不包含渲染后的文本内容。优化后先通过 `page.data()` 获取数据中的对应索引,再通过索引定位 DOM 元素,大幅提高测试稳定性。 --- **文档生成时间**: 2026-07-21 | **测试工具**: Jest 29 + miniprogram-automator + 微信开发者工具