@clawhub-wxl1779766474-da57fe1c4d
根据需求文档生成 XMind 格式测试用例。当用户要求"写测试用例"、"生成用例"、"写XMind用例"时使用。
---
name: xmind-testcase-generator
description: 根据需求文档生成 XMind 格式测试用例。当用户要求"写测试用例"、"生成用例"、"写XMind用例"时使用。
---
# XMind 测试用例生成规范
## 工作流程
1. **获取需求文档**:飞书链接用 feishu_fetch-doc,本地文件直接读。**必须重新拉取全文**,不能依赖对话摘要
2. **建立模块清单**:列出所有功能段落,确认无遗漏
3. **代码深度分析**(复杂功能必做):读核心代码文件,关注客户端常量/枚举/行为模式、服务端校验链/error code
4. **编写用例数据**:修改 gen_xmind.py 的 build_cases() 函数
5. **执行脚本**:`python3 gen_xmind.py`
6. **验证统计**:smoke 占比 ~20%(15%-25% 可接受)
## XMind 层级结构
```
需求名 → 模块 → 子模块 → 测试点(P1/P3)→ 步骤(带编号)→ 预期结果
```
## 优先级
| 标记 | 含义 | 占比 |
|------|------|------|
| P1 | smoke 冒烟用例 | ~20% |
| P3 | normal 普通用例 | ~80% |
- 只用 P1 和 P3,不用 P2
- 每条用例必须标注优先级
## 步骤编号格式
```
测试点(P1/P3)
├── "1、操作步骤"
│ └── "1、预期结果" ← 步骤的子节点
├── "2、操作步骤"
│ └── "2、预期结果"
```
## 标签(F3)
| 标签 | 平台 |
|------|------|
| 空 | iOS + Android + Server |
| `ai` | iOS + Android |
| `as` | Android + Server |
| `f` | 前端 FE/H5 |
## 备注(F4)— 前置条件
写在测试点节点上,不混入步骤:`note="前置条件:XXX"`
## 用例粒度原则
- 验证点不丢,执行路径连贯的可整合
- 同类枚举值合并成1条 case,步骤中逐个验证每个值
- 前置条件不同、主客态差异必须拆成独立 case
- `#` 开头的节点被忽略
## 容易遗漏的场景 Checklist
- 跨页面状态同步
- 操作撤回后重新触发
- 校验失败后流程不被绕过
- 前置状态变体(播放/静止、登录态/游客)
- 操作后关联功能状态联动
- 弹窗/页面所有按钮行为穷举
- 操作频率限制
- 并发/竞态场景
- 边界值(0/负数/小数)
- 负面清单(满足条件X才触发 → 验证不满足X时不触发)
- 活动时间边界(最后一刻的特殊逻辑)
- 同一功能的多种行为模式(从代码中发现)
- 服务端 error code 全覆盖
- 状态变更后所有关联 UI 区域全面更新
- 端到端请求链路追踪(客户端URL → 服务端handler)
## gen_xmind.py API
详见 reference.md
FILE:reference.md
# gen_xmind.py API 参考文档
## 文件结构
```
gen_xmind.py
├── 核心工具函数(不要修改)
│ ├── _mid() — 生成唯一 ID
│ ├── topic() — 创建 XMind 节点
│ ├── P1, P3 — 优先级常量
│ ├── steps() — 生成步骤→预期子节点列表
│ ├── case() — 创建测试用例
│ ├── sub_module() — 创建子模块节点
│ ├── module() — 创建模块节点
│ └── generate_xmind() — 生成 .xmind 文件
├── build_cases() — 用例数据(每次只修改这部分)
└── __main__ — 执行入口
```
---
## 常量
```python
P1 = ["priority-1"] # smoke 冒烟用例(~20%)
P3 = ["priority-3"] # normal 普通用例(~80%)
```
---
## 核心函数
### `topic(title, children=None, markers=None, note=None, labels=None)`
创建一个 XMind 节点。这是底层函数,通常不直接调用,而是通过 `case()`、`sub_module()`、`module()` 间接使用。
| 参数 | 类型 | 说明 |
|------|------|------|
| title | str | 节点标题 |
| children | list | 子节点列表 |
| markers | list | 优先级标记,如 `["priority-1"]` |
| note | str | F4 备注(前置条件) |
| labels | list | F3 标签,如 `["ai"]` |
### `steps(steps_list)`
生成带编号的 步骤→预期 子节点列表。
| 参数 | 类型 | 说明 |
|------|------|------|
| steps_list | list[tuple] | `[(步骤文本, 预期文本), ...]`,预期为空字符串 `""` 则该步骤无预期子节点 |
**示例:**
```python
steps([
("点击登录按钮", "弹出登录弹窗"), # 有预期
("输入账号密码", ""), # 无预期(前置操作)
("点击确认", "登录成功跳转首页"), # 有预期
])
```
生成结构:
```
├── "1、点击登录按钮"
│ └── "1、弹出登录弹窗"
├── "2、输入账号密码" ← 无子节点
└── "3、点击确认"
└── "3、登录成功跳转首页"
```
### `case(title, steps_list, priority=P3, note=None, labels=None)`
创建一条测试用例(测试点节点)。
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| title | str | - | 用例标题 |
| steps_list | list[tuple] | - | `[(步骤, 预期), ...]` |
| priority | list | P3 | `P1` 或 `P3` |
| note | str | None | 前置条件(F4 备注) |
| labels | list | None | 平台标签(F3),如 `["ai"]` |
**示例:**
```python
# 最简用例
case("功能入口正常展示", [
("打开APP首页", "功能入口按钮可见"),
])
# 完整用例(P1 + 前置条件 + 平台标签)
case("VIP用户专属功能展示",
[("进入个人中心", "展示VIP专属功能区"),
("点击专属功能", "正常进入功能页面")],
P1,
note="前置条件:登录VIP账号",
labels=["ai"])
# 无预期的中间步骤
case("多步骤操作流程",
[("打开设置页面", ""), # 前置步骤,无预期
("找到目标选项", ""), # 前置步骤,无预期
("点击开关", "功能状态切换")], # 最终验证
P3)
```
### `sub_module(name, cases_list)`
创建子模块节点。
| 参数 | 类型 | 说明 |
|------|------|------|
| name | str | 子模块名称 |
| cases_list | list | 用例列表或更深层子模块列表 |
**支持嵌套:**
```python
# 子模块直接包含用例
sub_module("基础功能", [
case("用例1", [...]),
case("用例2", [...]),
])
# 子模块包含更深层子 tab
sub_module("执行组件", [
sub_module("点踩", [case(...)]),
sub_module("复制文案", [case(...)]),
sub_module("语音播报", [case(...)]),
])
```
### `module(name, sub_modules)`
创建模块节点。
| 参数 | 类型 | 说明 |
|------|------|------|
| name | str | 模块名称 |
| sub_modules | list | 子模块列表 |
### `generate_xmind(root_title, modules_list, output_name)`
生成 `.xmind` 文件并输出统计信息。
| 参数 | 类型 | 说明 |
|------|------|------|
| root_title | str | 根节点标题(需求名) |
| modules_list | list | 模块节点列表 |
| output_name | str | 输出文件名(不含路径,自动输出到 `~/Desktop/工作/`) |
**输出示例:**
```
生成完成: /Users/xxx/Desktop/工作/XX模块_用例.xmind
用例统计: 总计 45 条, smoke 9 条 (20.0%), normal 36 条
```
---
## build_cases() 编写模板
每次生成新用例时,**只修改 `build_cases()` 函数的内容**,核心工具函数不动。
### 基础模板
```python
def build_cases():
"""构建用例数据,返回 (根标题, 模块列表, 输出文件名)"""
# ===== 子模块1: XXX =====
sub1 = [
case("用例标题1", [
("步骤1", "预期1"),
("步骤2", "预期2"),
], P1),
case("用例标题2", [
("步骤1", ""), # 前置操作无预期
("步骤2", "预期2"),
]),
]
# ===== 子模块2: YYY =====
sub2 = [
case("用例标题3", [
("步骤1", "预期1"),
], note="前置条件:XXX"),
]
# --- 组装 ---
modules_list = [module("模块名", [
sub_module("子模块1名", sub1),
sub_module("子模块2名", sub2),
])]
return "需求名-模块名", modules_list, "需求名_模块名_用例.xmind"
```
### 嵌套子模块模板
```python
def build_cases():
# 三级子模块
sub_a1 = [case("用例A1", [...]), case("用例A2", [...])]
sub_a2 = [case("用例A3", [...]), case("用例A4", [...])]
sub_a = sub_module("大子模块A", [
sub_module("小子模块A1", sub_a1),
sub_module("小子模块A2", sub_a2),
])
sub_b = [case("用例B1", [...]), case("用例B2", [...])]
modules_list = [module("模块名", [
sub_a,
sub_module("子模块B", sub_b),
])]
return "需求名", modules_list, "输出文件名.xmind"
```
---
## XMind 文件格式说明
生成的 `.xmind` 文件是一个 ZIP 压缩包,内含:
| 文件 | 说明 |
|------|------|
| content.json | 思维导图内容数据(节点树) |
| metadata.json | 元数据(创建者、活跃画布 ID) |
| manifest.json | 文件清单 |
无需安装任何第三方库,仅使用 Python 标准库:`json`、`zipfile`、`os`、`uuid`。
---
## 常见问题
### Q: 中文引号导致语法错误
A: Python 字符串内部的引号必须用英文引号(单引号 `'` 或转义双引号 `\"`),不能用中文引号 `""''`。
### Q: 如何调整输出目录
A: 修改 `generate_xmind()` 函数中的 `output_path`,默认输出到 `~/Desktop/工作/`。
### Q: smoke 占比不对怎么调
A: 检查每个子模块中 P1 的分配。原则:只有最核心的链路验证点标 P1,总量控制在 ~20%。
### Q: 如何忽略某些用例
A: 在用例标题前加 `#`,如 `case("#暂不测试的功能", ...)`,该节点及子节点会被忽略。
FILE:scripts/gen_xmind.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json, zipfile, os, uuid, sys
def _mid():
return uuid.uuid4().hex[:20]
def topic(title, children=None, markers=None, note=None, labels=None):
t = {"id": _mid(), "title": title, "structureClass": "org.xmind.ui.map.unbalanced"}
if children:
t["children"] = {"attached": children}
if markers:
t["markers"] = [{"markerId": m} for m in markers]
if note:
t["notes"] = {"plain": {"content": note}}
if labels:
t["labels"] = labels
return t
P1 = ["priority-1"]
P3 = ["priority-3"]
def steps(steps_list):
nodes = []
for i, (s, e) in enumerate(steps_list, 1):
step_node = topic(f"{i}、{s}", [topic(f"{i}、{e}")] if e else None)
nodes.append(step_node)
return nodes
def case(title, steps_list, priority=P3, note=None, labels=None):
return topic(title, steps(steps_list), priority, note=note, labels=labels)
def sub_module(name, cases_list):
return topic(name, cases_list)
def module(name, sub_modules):
return topic(name, sub_modules)
def generate_xmind(root_title, modules_list, output_name):
root = topic(root_title, modules_list)
content = [{"id": _mid(), "class": "sheet", "title": "画布 1", "rootTopic": root}]
output_path = os.path.expanduser(f"~/Desktop/工作/{output_name}")
if not output_path.endswith(".xmind"):
output_path += ".xmind"
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr("content.json", json.dumps(content, ensure_ascii=False, indent=2))
zf.writestr("metadata.json", json.dumps({
"creator": {"name": "Xmind"},
"activeSheetId": content[0]["id"]
}))
zf.writestr("manifest.json", json.dumps({
"file-entries": {"content.json": {}, "metadata.json": {}}
}))
def count_cases(node):
total, smoke = 0, 0
ms = node.get("markers", [])
if any(m["markerId"].startswith("priority-") for m in ms):
total += 1
if any(m["markerId"] == "priority-1" for m in ms):
smoke += 1
for child in node.get("children", {}).get("attached", []):
t, s = count_cases(child)
total += t; smoke += s
return total, smoke
total, smoke = count_cases(root)
pct = f"{smoke/total*100:.1f}%" if total else "0%"
print(f"生成完成: {output_path}")
print(f"用例统计: 总计 {total} 条, smoke {smoke} 条 ({pct}), normal {total - smoke} 条")
return output_path
# ============================================================
# 用例数据
# ============================================================
def build_cases():
# ========== 模块1:主页背景弹窗分流 ==========
mod1_弹窗展示 = [
case("点击主页背景区域弹出操作弹窗", [
("进入个人主页,点击背景图区域", "弹出操作弹窗,弹窗标题为选择操作方式"),
("弹窗包含四个操作入口", "弹窗展示【背包佩戴】【上传照片】【拍照】【选取文件】四个入口"),
], P1),
case("非登录用户点击主页背景", [
("游客态进入他人主页,点击背景图区域", "弹出操作弹窗,展示四个入口"),
], P1),
case("操作弹窗可正常关闭", [
("弹出弹窗后,点击弹窗外区域", "弹窗关闭"),
("弹出弹窗后,点击取消按钮", "弹窗关闭"),
], P3),
]
mod1_弹窗操作 = [
case("点击【背包佩戴】跳转商城背包主页背景tab", [
("点击背景区域弹出弹窗", ""),
("点击【背包佩戴】", "跳转至商城-背包-主页背景tab,商城去掉上传按钮"),
], P1, note="前置条件:用户已有主页背景商品在背包中"),
case("点击【上传照片】进入系统相册", [
("点击背景区域弹出弹窗", ""),
("点击【上传照片】", "唤起系统相册,可选择图片"),
], P1),
case("点击【拍照】打开相机", [
("点击背景区域弹出弹窗", ""),
("点击【拍照】", "打开相机,可拍摄照片"),
], P1),
case("点击【选取文件】打开文件管理器", [
("点击背景区域弹出弹窗", ""),
("点击【选取文件】", "打开文件管理器,可选择图片文件"),
], P3),
]
mod1_自定义上传 = [
case("上传自定义图片-相册选择并裁剪", [
("点击背景区域弹出弹窗", ""),
("点击【上传照片】", "唤起相册"),
("选择图片", "进入裁剪页面,裁剪比例3:2"),
("确认裁剪", "图片上传,应用为静态模式背景"),
], P3),
case("上传自定义图片-拍照并裁剪", [
("点击背景区域弹出弹窗", ""),
("点击【拍照】", "打开相机"),
("拍摄照片", "进入裁剪页面,裁剪比例3:2"),
("确认裁剪", "图片上传,应用为静态模式背景"),
], P3),
case("上传自定义图片-文件选择", [
("点击背景区域弹出弹窗", ""),
("点击【选取文件】", "打开文件管理器"),
("选择图片文件", "图片上传,应用为静态模式背景"),
], P3),
case("自定义上传图片后显示在主页", [
("完成自定义图片上传", "个人主页背景显示用户上传的图片"),
], P3),
]
mod1_上传错误 = [
case("上传非图片文件时提示错误", [
("点击背景区域弹出弹窗", ""),
("点击【选取文件】", ""),
("选择非图片文件(如txt)", "提示'自定义图片出错',不应用背景"),
], P3),
case("拍照权限被拒绝时提示", [
("授予相机权限后进入主页,点击背景区域", ""),
("在系统设置中关闭相机权限", ""),
("返回唱吧点击【拍照】", "提示相机权限不可用,无法拍照"),
], P3, note="前置条件:首次已授权相机权限"),
case("相册权限被拒绝时提示", [
("授予相册权限后进入主页,点击背景区域", ""),
("在系统设置中关闭相册权限", ""),
("返回唱吧点击【上传照片】", "提示相册权限不可用,无法选择照片"),
], P3, note="前置条件:首次已授权相册权限"),
]
# ========== 模块2:商城主页背景 ==========
mod2_Tab分类 = [
case("主页背景Tab展示三个分类", [
("进入商城-主页背景", "展示三个tab:热门推荐、会员免费专区、情侣专区"),
], P1),
case("点击热门推荐Tab选中并展示商品", [
("进入商城-主页背景,点击热门推荐Tab", "热门推荐Tab选中,展示该分类下商品"),
], P3),
case("点击会员免费专区Tab选中并展示商品", [
("进入商城-主页背景,点击会员免费专区Tab", "会员免费专区Tab选中,展示SVIP/VIP 0元商品"),
], P3),
case("点击情侣专区Tab选中并展示商品", [
("进入商城-主页背景,点击情侣专区Tab", "情侣专区Tab选中,展示情侣背景商品"),
], P3),
case("Tab切换时子tab和商品随之切换", [
("在热门推荐Tab切换子tab", "下方商品随之切换,直接在当前页面展示,不跳二级页面"),
], P3),
case("热门推荐Tab子tab选中放大效果", [
("点击热门推荐下的子tab", "该tab icon有选中放大效果"),
], P3),
]
mod2_热门推荐商品 = [
case("热门推荐Tab展示商品卡片2个一排", [
("进入商城-主页背景-热门推荐Tab", "展示商品卡片,2个一排布局"),
], P3),
case("限时限购角标-倒计时显示规则", [
("后台配置限时商品,上架时间≤7天", "商品展示【限时】角标和倒计时"),
("倒计时剩余>1天时", "显示D+天数+时分秒"),
("倒计时剩余<1天时", "自动变为hh:mm:ss格式"),
("倒计时剩余<1小时时", "自动变为mm:ss格式"),
("倒计时归零时", "角标和倒计时自动消失"),
], P3),
case("限购角标-库存不足100时显示", [
("后台设置商品每天库存≤100", "商品展示【剩余X个】角标,显示当前库存数"),
("库存为0时", "角标变为【售罄】,点击购买提示'当前商品已售罄'"),
], P3),
case("限时和限购角标优先级", [
("后台同时配置限时和限购,且库存≤100", "角标优先级:售罄>限购>限时,建议两者不同时配置"),
], P3),
case("热门推荐商品卡片点击唤起半屏预览", [
("点击热门推荐下的商品卡片", "唤起半屏预览,不跳转二级页面"),
], P3),
case("展开更多-收起更多", [
("点击模块下的【展开更多】", "展示该配置下所有商品"),
("点击【收起更多】", "收起商品,仅展示配置下前3个商品"),
], P3),
case("模块下无商品时不显示该模块", [
("某模块下无商品", "该模块不在热门推荐Tab中展示"),
], P3),
case("限时商品上架超7天不允许添加", [
("热门推荐模块添加商品时,上架时间距今超过7天", "该商品不允许添加到热门推荐模块,后台提示或过滤"),
], P3),
case("热门推荐展示类别角标而非等级角标", [
("热门推荐Tab下商品有多种类型", "统一展示类别角标,不展示等级角标"),
], P3),
]
mod2_会员免费专区 = [
case("会员免费专区展示迁移后的历史商品", [
("进入会员免费专区Tab", "展示:倾城之恋、万物春醒、狗拉雪橇、御气而行、心动信号、风吹麦浪、霓虹街区、浪漫星球、冬季恋歌-女生版、冬季恋歌-男生版"),
], P3),
case("会员免费专区仅展示SVIP/VIP 0元商品缩略图", [
("SVIP会员进入会员免费专区", "展示SVIP 0元使用商品缩略图,不展示头像"),
("VIP会员进入会员免费专区", "展示VIP 0元使用商品缩略图,不展示头像"),
("非会员进入会员免费专区", "不展示任何商品或引导开通会员"),
], P1),
case("会员免费专区内SVIP商品需升级购买", [
("VIP会员点击免费专区SVIP专属商品", "提示'此为SVIP专属商品,请升级SVIP'"),
], P3),
]
mod2_情侣专区 = [
case("情侣专区展示情侣背景商品2个一排", [
("进入情侣专区Tab", "展示情侣专属背景商品,2个一排布局"),
], P3),
case("情侣专区商品购买后发放情侣背景", [
("购买情侣专区商品", "情侣背景发放至背包,双方均可使用"),
], P3),
]
mod2_首页特殊 = [
case("首页配置主页背景时单屏展示规则", [
("首页品类模块配置了主页背景商品", "单屏展示1个主页背景+1个其他装扮,前端需适配"),
], P3, note="前置条件:首页品类模块配置了主页背景"),
]
# ========== 模块3:会员专属页面展示 ==========
mod3_特权标题 = [
case("VIP和SVIP会员专属特权标题增加变更文案", [
("VIP会员进入专属特权页面", "标题后方展示文案'(权益不定期更新,请以页面展示为准)'"),
("SVIP会员进入专属特权页面", "标题后方展示文案'(权益不定期更新,请以页面展示为准)'"),
], P3),
]
mod3_个性装扮模块 = [
case("会员个性装扮模块展示SVIP/VIP 0元商品缩略图", [
("VIP会员进入个性装扮模块", "展示VIP 0元使用商品缩略图,不展示头像"),
("SVIP会员进入个性装扮模块", "展示SVIP 0元使用商品缩略图,不展示头像"),
], P3),
]
mod3_详情页 = [
case("个性装扮详情页标题变更为主页背景图", [
("进入个性装扮详情页-主页背景模块", "页面标题显示为主页背景图"),
], P3),
case("主页背景图详情页展示SVIP/VIP 0元商品缩略图", [
("SVIP会员进入主页背景图详情页", "展示SVIP 0元使用商品缩略图"),
("VIP会员进入主页背景图详情页", "展示VIP 0元使用商品缩略图"),
], P3),
]
mod3_SVIP页面 = [
case("SVIP专属特权页面示例图去掉静态和动态模式", [
("进入SVIP专属特权页面-装扮特权", "示例图中不包含静态模式和动态模式背景图"),
], P1),
]
# ========== 模块4:商城主页背景去模式 & 历史遗留 ==========
mod4_去模式 = [
case("商城主页背景去掉动效模式和静态模式切换", [
("进入商城-主页背景", "不再区分动效模式和静态模式Tab,统一展示"),
], P1),
case("背包中历史动效/静态模式商品仍正常展示和使用", [
("用户在商城下架前已购买动效模式或静态模式商品", "商品仍在背包中展示,可正常佩戴使用"),
], P1),
]
mod4_迁移通知 = [
case("下线前7天通知会员商品即将下架", [
("商品下架前7天,VIP/SVIP会员进入个性装扮页面", "收到通知:告知静态/动态模式商品将于X月X日下架,请尽快购买使用"),
], P3, note="前置条件:会员在装扮商城有未购买的历史商品"),
case("下线前7天商品可在商城0元购买", [
("收到下线通知后,VIP/SVIP会员进入商城", "静态模式和动态模式商品仍可0钻购买,购买后发放至背包"),
], P3),
]
mod4_下架后处理 = [
case("下架后前端隐藏静态模式和动态模式商品", [
("7天通知期满后,VIP/SVIP会员进入商城", "静态模式和动态模式商品在前端隐藏,不展示"),
], P1),
case("下架后商品自动发放至对应等级会员背包", [
("7天通知期满后,VIP会员进入背包", "VIP等级对应的静态/动态模式商品已发放至背包"),
("7天通知期满后,SVIP会员进入背包", "SVIP等级对应的静态/动态模式商品已发放至背包"),
], P1),
case("下架后会员在个人主页仍能使用已购背景", [
("下架后,VIP会员已佩戴历史背景商品", "个人主页正常展示已购买的背景"),
], P1),
case("下架后会员购买记录保留", [
("下架后查看历史购买记录", "静态/动态模式商品购买记录仍保留"),
], P3),
]
# ========== 模块5:商品详情页-礼盒关联主页背景 ==========
mod5_礼盒 = [
case("礼盒关联主页背景时详情页展示内容预览", [
("后台配置礼盒,关联子商品包含主页背景", "商品详情页展示该礼盒下所有装扮,角标显示装扮类别"),
("滑动礼盒详情页", "展示更多礼盒内商品"),
], P3),
case("礼盒内装扮点击仅预览不展示购买", [
("在礼盒详情页点击礼盒内主页背景商品", "唤起半屏弹窗,仅展示效果,不展示购买按钮、价格档位"),
], P3),
]
# ========== 组装 ==========
modules_list = [
module("主页背景弹窗分流", [
sub_module("弹窗展示", mod1_弹窗展示),
sub_module("弹窗操作入口", mod1_弹窗操作),
sub_module("自定义上传", mod1_自定义上传),
sub_module("上传错误处理", mod1_上传错误),
]),
module("商城主页背景", [
sub_module("Tab分类", mod2_Tab分类),
sub_module("热门推荐商品", mod2_热门推荐商品),
sub_module("会员免费专区", mod2_会员免费专区),
sub_module("情侣专区", mod2_情侣专区),
sub_module("首页特殊处理", mod2_首页特殊),
]),
module("会员专属页面展示", [
sub_module("会员特权标题文案变更", mod3_特权标题),
sub_module("会员个性装扮模块", mod3_个性装扮模块),
sub_module("个性装扮详情页", mod3_详情页),
sub_module("SVIP专属特权页面", mod3_SVIP页面),
]),
module("商城主页背景去模式", [
sub_module("去动效/静态模式", mod4_去模式),
sub_module("历史商品迁移通知", mod4_迁移通知),
sub_module("下架后处理", mod4_下架后处理),
]),
module("商品详情页-礼盒关联主页背景", [
sub_module("礼盒关联主页背景展示", mod5_礼盒),
]),
]
return "装扮迭代1.0-主页背景", modules_list, "装扮迭代1.0_主页背景_用例"
if __name__ == "__main__":
root_title, modules_list, output_name = build_cases()
generate_xmind(root_title, modules_list, output_name)
FILE:xmind-testcase.md
# XMind 测试用例生成规范
> 本文件由 SKILL.md 自动生成,请勿手动编辑。
> 修改请编辑 skills/write-cases/SKILL.md,提交时会自动同步。
>
> 本规范适用于所有 AI 编程助手(Claude Code、Cursor、Windsurf、GitHub Copilot 等)。
> 当用户要求"写测试用例"、"生成用例"、"写 XMind 用例"时,严格按照以下规范执行。
---
## 概述
根据需求文档(飞书/Confluence/本地文件等)自动生成标准化的 XMind 格式测试用例文件。
本技能使用 Python 脚本生成 `.xmind` 文件(ZIP 格式,包含 content.json/metadata.json/manifest.json),无需安装任何第三方库。
---
## 完整工作流程
### 第一步:理解需求
1. **获取需求文档**:如果用户提供飞书链接,拉取文档内容;如果是本地文件直接读取。**每次写用例前必须重新拉取原始文档全文,不能依赖对话摘要或上次的记忆**——摘要会丢失细节,尤其是具体文案、边界规则、时间节点等。
2. **建立模块清单**:先把需求文档中所有功能段落列出来,每个段落对应一个模块,确认无遗漏后再开始写用例。
3. **仔细阅读需求,遇到不理解的立即提问**:逐段分析,提取所有功能点、交互规则、边界条件、异常处理。**凡是需求描述模糊、逻辑不完整、或存在多种理解的地方,必须先向用户提问确认,不能自己假设**——错误的理解会导致用例方向整体偏差。
4. **不遗漏细节**:需求文档中的每个描述、每个条件分支、每个状态转换都要覆盖
### 第二步:代码深度分析(强烈推荐,复杂功能必做)
如果有代码仓库,**必须深入阅读核心代码文件**,不能只 Grep 关键字就跳过。尤其是涉及多种行为模式的功能(如送礼、支付、消息发送),必须把客户端和服务端核心逻辑读完再写用例。
#### 客户端代码重点关注:
1. **行为模式枚举**:同一功能入口可能有多种执行路径(如送礼有单次送/批量送/连击送/全麦送),需求文档往往只描述主流程,必须从代码中发现所有分支
2. **常量和枚举值**:按钮类型(如 SEND_BUTTON_TYPE_ITEM=1 vs NORMAL=10)、模式常量 → 每种模式都需要对应用例
3. **UI 交互参数**:定时器时长(如连击 5 秒倒计时)、触发间隔(如 100ms 自动发送)、数量上限(如 1314) → 成为用例的具体断言值
4. **动画/特效触发条件**:档位动画(如 3/5/66/188/520/1314 触发不同动效) → 关键档位需要覆盖
#### 服务端代码重点关注:
1. **校验链和 error code**:逐个读校验方法,每个 error code 都要有对应的测试场景(如 5001 参数错误、5002 余额不足、5003 礼物下架、5011 冷却中、5012 配额用尽、5013 不在麦上)
2. **防并发/防重机制**:Redis 原子锁、uniq_key 幂等、TTL 窗口 → 这些是服务端专有逻辑,需求文档不会提及
3. **批量操作的计费逻辑**:总价计算公式(如全麦送礼 = 单价 × 数量 × 人数)、配额检查用总数还是单次数 → 关系到金额正确性
4. **异步处理**:哪些操作是同步返回、哪些走消息队列(如排行榜更新走 NSQ 异步) → 影响数据一致性验证时机
#### 端到端请求链路追踪(必做)
分析任何客户端功能时,**必须从客户端代码中提取实际请求的 URL/endpoint**,然后在服务端项目中找到对应的 handler 文件并完整阅读。不能仅靠关键词搜索服务端代码——关键词搜索容易找到"相关但非核心"的文件,而漏掉真正的决策入口。
**操作步骤**:
1. 在客户端代码中找到功能的网络请求调用,提取请求 URL(如 `/login/check_code.php`、`/api/gift/send`)
2. 根据 URL 路径在服务端项目中定位对应的 handler 文件(如 `ktvserver/login/check_code.php`)
3. **完整阅读** handler 文件,重点关注:条件分支、Redis/DB 查询、决策逻辑、返回值差异
4. 如果 handler 调用了其他 Service/Model,继续追踪关键调用链
**为什么必须这样做**:
- 关键词搜索(如搜"验证码")会找到多个相关文件,但真正的决策逻辑可能在一个完全不含该关键词的 handler 文件中
- 例:搜索"滑块验证"找到了 ShumeiSlideService.php 和 RiskControlService.php,但真正决定"是否弹出验证码"的逻辑在 check_code.php 的 Redis 查询中(检查 `checkcode_used:{IP}` key 是否存在),该文件不含"滑块"关键词
#### 代码分析产出要求:
- 从代码中发现的字段 → 边界值要覆盖
- 从代码中发现的状态常量 → 状态转换要覆盖
- 从代码中发现的接口参数差异(如主客态)→ 对应验证
- 从代码中发现的校验逻辑 → 每条校验规则一条用例
- **不能只看需求文档写用例**,代码中的实现细节往往是用例精度的关键来源
### 第三步:编写用例数据
修改 `gen_xmind.py` 的 `build_cases()` 函数(**只修改这个函数,核心工具函数不动**)。
### 第四步:执行脚本生成 XMind 文件
```bash
python3 gen_xmind.py
```
输出到当前目录,打印用例统计(总数、smoke 数、占比)。
### 第五步:验证统计
- 确认 smoke 占比在 ~20%(15%-25% 可接受)
- 确认每条用例都标注了优先级
- 确认没有遗漏需求点
### 第六步:输出涉及代码位置(按端分类)
用例生成后,必须输出本次需求涉及的历史代码位置,按各端项目维度整理,格式如下:
```
## 本次需求涉及代码位置
### Android(Changba2)
- 模块/功能点:文件路径:行号 — 说明
### 服务端(ktvserver)
- 模块/功能点:文件路径 — 说明
### iOS(如有)
- 模块/功能点:文件路径:行号 — 说明
### 前端/H5(如有)
- 模块/功能点:文件路径 — 说明
```
**目的**:让测试人员在验证时能快速定位相关代码,辅助理解实现细节和边界逻辑,也便于开发 review 时对照。
---
## XMind 层级结构(核心规范)
```
需求名(根节点)
└── 模块名
└── 子模块名
└── (更深层子 tab,层级不限)
└── 测试点(标注优先级 P1/P3)
└── 步骤节点(带编号 "1、操作步骤")
└── 预期结果节点(带编号 "1、预期结果")
```
### 层级说明
| 层级 | 说明 | 示例 |
|------|------|------|
| 根节点 | 需求名/项目名 | "唱宝AI一期-会话区" |
| 模块 | 大功能模块 | "会话区"、"内容输入区" |
| 子模块 | 模块内的功能分组 | "伴奏组件"、"作品组件" |
| 更深子 tab | 子模块内还可继续拆分 | "点踩"、"复制文案"、"语音播报" |
| 测试点 | 一条具体的测试用例 | "伴奏列表一级最多展示5条" |
| 步骤 | 操作步骤(带编号) | "1、点击伴奏封面区域" |
| 预期结果 | 步骤的子节点(带编号) | "1、开始播放伴奏" |
**关键规则**:
- 模块内**必须**用子模块分组,不能把用例直接挂在模块下
- 子模块下还可以继续拆分子 tab,层级不限
- 测试点是带有优先级标记的节点
---
## 优先级标记规范
### 只使用两种优先级
| 标记 | 含义 | XMind marker | 占比 |
|------|------|-------------|------|
| P1 | smoke 冒烟用例 | `priority-1` | ~20% |
| P3 | normal 普通用例 | `priority-3` | ~80% |
### 硬性规则
1. **每条测试用例必须标注优先级**,不能有未标注的节点
2. **不使用 priority-2**,只有 P1 和 P3
3. **smoke 占比控制在 ~20%**(15%-25% 可接受范围)
4. **P1 只标注最核心的链路验证点**:
- 主流程的关键节点(如:发送消息→AI回复、点击播放→播放伴奏)
- 核心功能入口(如:冷启动进入页面、点击核心按钮)
- 关键异常处理(如:断网重试、次数耗尽提示)
---
## 步骤与预期结果编号规范
### 编号格式
- 步骤:`1、操作步骤`、`2、操作步骤`、`3、操作步骤`...
- 预期:`1、预期结果`、`2、预期结果`、`3、预期结果`...
### 结构关系
```
测试点节点(带 P1/P3 标记)
├── "1、点击XX按钮" ← 步骤1
│ └── "1、弹出XX弹窗" ← 步骤1的预期结果
├── "2、选择XX选项" ← 步骤2
│ └── "2、选项高亮显示" ← 步骤2的预期结果
└── "3、点击确认" ← 步骤3
└── "3、操作成功提示" ← 步骤3的预期结果
```
### 重要规则
1. 预期结果**是步骤节点的子节点**(不是同级节点)
2. 预期编号**必须与对应步骤编号一致**
3. 某步骤的预期结果**可以为空**(该步骤没有子节点),常用于前置操作步骤
4. 预期结果之后的内容会被忽略(多余子节点不解析)
---
## 标签(F3)规范
| 标签值 | 适用平台 |
|--------|----------|
| 空(不设标签) | iOS + Android + Server 全平台 |
| `ai` 或 `ia` | iOS + Android 客户端 |
| `as` 或 `sa` | Android + Server |
| `f` | 前端(FE/H5) |
---
## 备注(F4)规范 — 前置条件
- F4 备注**专用于标注前置条件**
- **不要**把前置条件混写在步骤中
- 前置条件写在**测试点节点**上(不是步骤节点上)
### 示例
```python
case("VIP用户购买商品享受折扣",
[("选择商品点击购买", "显示VIP折扣价格"),
("确认支付", "支付成功,实际扣款为折扣价")],
P1,
note="前置条件:登录VIP账号,账户余额充足")
```
---
## 用例编写粒度原则(核心!)
### 核心原则:验证点不能丢,但执行路径连贯的可以整合
**什么时候可以合并:**
- 同一页面内顺序可验证的同类检查(如逐个检查6种角标)
- 同一操作流程中的多步验证(如购买流程:选档位→购买→查看背包)
- 同类枚举值:合并成1条 case,但步骤中必须逐个验证每个值
- 后台校验规则(0/负数/小数/非数字):合并成1条,每种非法输入作为独立步骤
**什么时候必须拆成独立 case:**
- 前置条件不同的(如 VIP vs 非VIP,需要切换账号)
- 主客态差异(需要切换视角)
- 执行路径不连贯、无法顺序完成的
- 不同维度的验证(如"角标展示"和"购买流程")
**禁止:**
- 写多模块时不自觉地降低粒度,丢掉验证点
- 把验证点合并到看不出来的程度(如"功能正常"这种笼统描述)
### 代码分析驱动的用例补充
- 从代码中发现的字段 → 边界值要覆盖
- 从代码中发现的状态常量 → 状态转换要覆盖
- 从代码中发现的接口参数差异 → 要有对应验证
---
## #开头节点
以 `#` 开头的节点标题表示该节点及其所有子节点将被忽略(不纳入用例统计和测试范围)。用于临时注释掉不需要的用例。
---
## 容易遗漏的场景 Checklist
写完主流程用例后,**必须逐条过一遍**,确认有没有以下遗漏:
### ✅ 1. 跨页面/跨场景状态同步
> A 页面的操作改变了某个状态,B 页面是否也感知到?
- 凡是涉及"状态变更"的操作(勾选、同意、购买…),要想到该状态在其他关联页面是否同步
- 例:在账密登录页同意协议 → 返回一键登录页,协议勾选状态是否联动
### ✅ 2. 操作撤回后重新触发
> 用户拒绝/取消某操作后,再次触发同一流程,是否还能正常走通?
- 凡是有"拒绝/取消/关闭"按钮的场景,必须补一条"撤回后重新触发"的用例
- 例:点[不同意]关闭协议弹窗 → 再次点击登录入口,是否仍能弹出弹窗
### ✅ 3. 失败/拒绝后流程不被绕过
> 验证/校验失败后,用户是否能跳过该环节直接进入下一步?
- 凡是有"拦截/校验/验证"性质的功能,失败后必须验证流程的强制性
- 例:滑块验证失败 → 不会跳过验证环节,必须重新验证才能继续
### ✅ 4. 前置状态变体
> 进入同一操作时,起始状态不同,结果是否一致?
- 当功能与某种状态强相关时(播放/静止、登录态/游客、全局播放器开着/关着),要枚举关键起始状态变体
- 例:进入二级页面时声音暂停 → 补充「关注页面在播放时进入」和「全局播放器播放时进入」两个变体
### ✅ 5. 操作路径延伸
> 需求文档里写了哪些触发路径,有没有漏掉相关路径?
- 对于有触发条件的功能,要想到所有能触发同一逻辑的路径,不只是主流程描述的那几个
- 例:头像列表刷新 → 文档只写了"进入feed"和"启动app",需补充「从二级页面返回」也触发刷新
### ✅ 6. 操作后关联功能的状态联动
> 操作A完成后,与A有交叉的功能B的状态是否也正确?
- 不要只验证操作本身最显眼的结果,还要检查该操作对其他关联功能的影响
- 例:选中头像 → 除了验证颜色变化,还要验证「选中后红点状态」和「消除红点后返回关注页的状态」
### ✅ 7. 动态内容验证(UI 展示的具体值是否正确)
> "样式符合设计稿"不够——展示的内容本身是否随数据正确变化?
- 凡是 UI 上有动态文案/动态图片的地方,必须验证其具体内容,而不是只验证"有展示"
- 例:宣发弹窗的文案是「昨日伤害全服第xx名,初始段位xx」,必须验证文案根据前一日 boss 等级动态变化,而不是只验证弹窗弹出
### ✅ 8. 弹窗/页面内所有按钮行为穷举
> 一个弹窗有几个按钮,每个按钮都要有对应用例
- 不能只测主操作按钮,次要按钮(关闭、跳转、辅助功能)同样要覆盖
- 例:宣发弹窗有「送礼攻击boss」和「关闭」两个按钮,只测了主按钮,漏了「点击送礼按钮弹起礼物箱并关闭弹窗」
### ✅ 9. 本地存储行为边界
> 本地存储(非服务端)的标记,卸载/升级后是否正确重置?
- 凡是"每设备只触发一次"或"已读/已显示"类逻辑,必须验证:卸载重装后是否重新触发、版本升级后是否触发新版本流程
- 例:宣发弹窗本地记录已显示 → 卸载重装后应重新弹出;升级新版本后应弹出新版弹窗
### ✅ 10. 操作频率限制
> 短时间内快速重复同一操作,是否有防抖/限速保护?
- 凡是有"确认操作"性质的功能(切换、提交、扣款),必须补一条快速重复触发的用例
- 例:快速切换 boss → 应提示「操作太快了,请稍候再试」,而不是直接执行多次
### ✅ 11. 并发/竞态场景
> 两个操作几乎同时触发时,系统如何处理?
- 当同一数据/状态可以被多条路径同时修改时,必须考虑并发场景
- 例:boss 被打爆触发自动升级,同时房主手动切换降级 → 以先后顺序处理,需验证两种先后顺序的结果
### ✅ 12. 计算规则的取整和最小值边界
> 有计算公式的功能,取整方向和最小有效值是否正确?
- 凡是需求中出现"按比例计算"、"取整"、"不足xx不计",必须设计边界值用例
- 例:奖池注入向下取整 → 单次计算不足 1 金币时不注入;需验证恰好等于 1 和少于 1 两种场景
### ✅ 13. 负面清单(明确什么操作不触发)
> 功能的触发条件以外的场景,是否真的不触发?
- 需求说"满足条件X才触发",就要补充"不满足条件X时不触发"的反向验证
- 例:只有攻击 boss 的礼物才注入奖池 → 需逐一验证:赠给非 boss 的礼物、背包礼物、其他玩法消耗金币均不增加奖池
### ✅ 14. 活动时间边界的特殊处理
> 活动/玩法的最后时刻,行为是否与常规时段不同?
- 凡是有开始/结束时间的功能,必须验证边界时刻(最后一轮、截止前最后一次操作)的特殊逻辑
- 例:19:30 最后一轮打爆 boss,不触发升级而是直接关闭玩法;与常规时段打爆后会升级的行为不同
### ✅ 15. 同一功能的多种行为模式全覆盖
> 同一个功能入口,是否存在多种执行路径/模式?
- 凡是涉及"发送/提交/操作"类功能,必须从代码中确认是否存在多种行为模式,每种模式独立写用例
- 例:送礼功能看起来只有一个"发送"按钮,但代码中实际有 4 种模式:单次送(giftnum=1)、批量送(选数量档位,单次请求 giftnum=N)、连击送(canCombo=1,按住自动发送)、全麦送(给所有麦位用户)——需求文档只描述了"送礼",4 种模式全靠读代码发现
- **检查方法**:在核心 Manager/Service 类中搜索所有 public 方法和条件分支,确认没有遗漏的执行路径
### ✅ 16. 服务端 error code 全覆盖
> 服务端有哪些校验点和错误码,每个 error code 是否都有对应的触发用例?
- 逐一阅读服务端校验方法,列出所有 error code 及其触发条件,每个 code 至少一条用例
- 例:送礼接口服务端有 8 种 error code(5001 参数错误、5002 余额不足、5003 礼物下架、5011 冷却中、5012 配额用尽、5013 不在麦上、5016 剩余不足、5999 背包错误),v2 版用例只覆盖了 5002,v3 版全部覆盖
- **检查方法**:在服务端代码中搜索 `setResultData` / `throw` / `return error` 等模式,收集完整 error code 列表
### ✅ 17. 状态变更后所有关联 UI 区域全面更新
> 一个核心状态发生变更(如升级/切换),依赖它的所有 UI 区域是否都同步更新?
- 操作完成后,要列出所有展示该状态的 UI 位置,逐一验证是否全部刷新
- 例:boss 升级后,需逐一验证:斩杀线进度、房间排行榜、用户排行榜、MVP 用户、MVP 房间、底部伤害信息、最小化时的图片资源,全部更新为新等级数据
### ✅ 18. 端到端请求链路追踪验证
> 客户端发出的每个关键请求,是否都追踪到了服务端 handler 并完整阅读?
- 不能仅靠关键词搜索服务端代码,必须从客户端代码中提取实际请求 URL,定位到服务端对应 handler 文件
- 关键词搜索容易找到"相关但非核心"的文件,真正的决策逻辑可能在一个完全不含搜索关键词的文件中
- 例:分析登录验证码功能,搜索"滑块验证"找到了 ShumeiSlideService 和 RiskControlService,但真正的"是否需要验证码"决策在 check_code.php 中(通过 Redis key `checkcode_used:{IP}` 判断),该文件不含"滑块"关键词,只能通过追踪客户端请求 URL `/login/check_code.php` 才能找到
- **检查方法**:列出本次分析中客户端发出的所有关键请求 URL,逐一确认对应的服务端 handler 是否已完整阅读
---
## gen_xmind.py 脚本 API
详见插件目录下 skills/write-cases/reference.md
---
## Changelog
### v1.6.0 (2026-03-12)
- 第二步「代码深度分析」新增「端到端请求链路追踪」必做步骤:分析客户端功能时必须提取实际请求 URL,定位服务端 handler 文件并完整阅读,不能仅靠关键词搜索
- Checklist 新增 1 条(#18):端到端请求链路追踪验证
- 来源:登录基础体验优化脑图编写。分析验证码功能时,关键词搜索找到了 ShumeiSlideService/RiskControlService 等"相关但非核心"文件,漏掉了真正的决策入口 check_code.php(通过 Redis key 判断是否弹出验证码)。用户通过实际测试发现遗漏后,总结出「端到端请求链路追踪」方法论
### v1.5.0 (2026-03-12)
- 第二步「代码分析」大幅扩充为「代码深度分析」:新增客户端 4 项关注点(行为模式枚举、常量枚举值、UI 交互参数、动画触发条件)+ 服务端 4 项关注点(校验链和 error code、防并发机制、批量计费逻辑、异步处理)
- Checklist 新增 2 条(#15-#16):同一功能多种行为模式全覆盖、服务端 error code 全覆盖
- 来源:鸿蒙版播放页礼物用例 v2→v3 重写。用户反馈「没有好好读代码」,通过深入阅读 NewGiftBoxManager.java(58KB)、ComboSendWidget、SendGiftService.php 等核心文件,发现送礼有 4 种行为模式(单次/批量/连击/全麦)、服务端有 8 种 error code,用例从 98 条增至 148 条
### v1.4.0 (2026-03-12)
- 工作流第一步:阅读需求时遇到不理解/模糊/多义的地方,必须先向用户提问,不能自行假设
- 工作流新增第六步:用例生成后按端输出本次涉及的历史代码位置(Android/服务端/iOS/前端),便于测试对照实现细节
### v1.3.0 (2026-03-12)
- 工作流第一步新增强制要求:每次必须重新拉取原始文档,不能依赖对话摘要;先建立模块清单再写用例
- Checklist 新增 9 条(#7-#15):动态内容验证、按钮行为穷举、本地存储边界、操作频率限制、并发竞态场景、计算取整边界、负面清单、活动时间边界、状态变更后全区域联动
- 来源:打boss 3.0 用例评审,用户补充 39 条被遗漏用例后的复盘总结
### v1.2.0 (2026-03-11)
- Checklist 新增 3 条:前置状态变体、操作路径延伸、操作后关联功能状态联动
- 来源:关注关系体验迭代2.0 用户补充用例的规律总结
### v1.1.0 (2026-03-11)
- 新增「容易遗漏的场景 Checklist」:跨页面状态同步、操作撤回后重新触发、失败不绕过流程
- 来源:登录基础体验优化需求用例评审中用户补充的 3 条思路
### v1.0.0
- 初始版本:XMind 层级结构、优先级规范、步骤编号规范、标签规范、粒度原则