313 lines
10 KiB
Markdown
313 lines
10 KiB
Markdown
# 小程序端 — 测试文档
|
||
|
||
> **位置**: `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: '<rootDir>/e2e/globalSetup.js',
|
||
setupFilesAfterFramework: ['<rootDir>/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 + 微信开发者工具
|