markdown
复制代码
**中间件测试框架 --- Skill 编写与设计经验报告**
---
> **背景:** 缓存服务拥有近 200 个 API 接口,每个接口都需要编写结构相似但细节各异的集成测试用例。我们基于 Claude Code 构建了一个专用 Skill(技能插件),让 AI 按照统一的架构规范和业务规则自动生成测试代码。本文总结这一 Skill 从设计、编写到迭代优化的全过程实践经验。
---
## 一、为什么云缓存测试需要 Skill
### 1.1 云缓存产品的测试特点
云缓存(Redis)作为中间件服务,API 接口按业务域天然分为六大类,每类都有**高度相似的测试模式**但**参数细节各不相同**:
| 业务域 | 典型操作 | 测试模式 |
|--------|----------|----------|
| 实例管理 | 创建/删除/重启/变配实例 | 状态等待 + 异步任务跟踪 |
| 安全访问 | 白名单分组增删改查、TLS 配置、账户管理 | CRUD 闭环 + 跨 API 交叉验证 |
| 监控分析 | 慢日志查询、离线分析 TopKeys | 时间参数格式处理 + 分页 |
| 备份恢复 | 创建备份/恢复实例/备份文件查询 | 异步操作等待 + 状态轮询 |
| 配置管理 | 参数配置/模板管理/模块加载卸载 | 全局配置修改需还原 |
| 任务管理 | 常规任务/诊断任务/诊断策略 | 两套独立任务系统区分 |
### 1.2 手工编写的三个痛点
- **效率低** --- 每个 API 的测试代码结构相似,但 SDK 参数、响应字段、断言逻辑各不相同,逐个手写重复劳动多
- **规范不统一** --- 不同人写的测试在架构分层、断言粒度、日志格式上风格各异
- **经验难以沉淀** --- SDK 参数签名不统一、时间格式差异、字段名与 API 名不匹配等"坑"被反复踩
### 1.3 Skill 的价值定位
Skill 不是一个简单的代码模板,而是一本**会自我进化的操作手册**:
- **手册** --- 定义了工作流(怎么做)、代码模板(做成什么样)、规则(什么不能做)
- **自我进化** --- 每次遇到新问题自动追加到经验库,高频问题自动提升为一级规则
---
## 二、Skill 的整体设计
### 2.1 文件结构设计
```
.claude/skills/cache-test-generator/
├── SKILL.md # 主文件(约 516 行):每次触发自动加载
│ ├── description (触发条件)
│ ├── Workflow (工作流)
│ ├── API Classification (业务域分类表)
│ ├── Key Architecture Rules (架构约束)
│ ├── Test File Template (代码模板)
│ ├── Common Pitfalls to Avoid (15 条一级规则)
│ └── Reference Documents System (参考文档索引)
│
└── references/ # 参考文档:工作流第一步主动读取
├── lessons_learned.md # 踩坑经验库(10 个陷阱 + 3 个最佳实践 + 排查清单)
├── error_debugging_guide.md # 排查指南(10 种常见错误 + 调试流程)
├── complete_example.md # 完整可运行示例 + 架构路径参考
└── scenario_testing_guide.md # 场景型测试指南(4 种 CRUD 模式)
```
> **实践经验**
>
> **主文件 vs 参考文档的分工**:SKILL.md 每次自动加载,所以只放"必须每次看到"的内容(工作流、模板、一级规则);详细案例和背景放 references/ 目录,工作流中要求"先读再动手"。这样既保证核心规则不遗漏,又不让主文件过于臃肿。
### 2.2 触发条件设计
SKILL.md 头部的 `description` 字段决定何时激活。我们的实践是**宽泛 + 具体并用**:
```
description: >
当用户要求 编写/创建/生成 缓存服务测试用例时触发。
当用户提到测试某个具体 API(如"写 DescribeXxxRequest 的测试")时触发。
当用户引用 apis_summary.md 或询问未测试的 API 时触发。
当用户要求提升测试覆盖率时触发。
```
这样设计既能覆盖"写个测试"这样的模糊请求,也能匹配"给 DescribeProxySlowLogRequest 写测试"这样的精确请求。
### 2.3 核心设计理念:四个要素
| 要素 | 解决什么问题 | 在 Skill 中如何实现 |
|------|-------------|-------------------|
| **工作流** | AI 按什么步骤做事,不跳步不漏步 | SKILL.md → Workflow 章节(单 API 10 步 / 场景型 8 步) |
| **模板** | 生成的代码结构完全一致 | SKILL.md → Test File Template(含完整代码骨架) |
| **规则** | 划清"绝对不能做"的红线 | SKILL.md → Common Pitfalls(15 条硬性规则) |
| **经验** | 前人踩过的坑不再重踩 | references/lessons_learned.md(10 个实战案例) |
---
## 三、工作流设计:让 AI 按步骤做事
工作流是 Skill 的骨架。没有工作流,AI 会直接生成代码,跳过"读 SDK 源码确认参数"、"检查 Service 层是否已有方法"等关键步骤。
### 3.1 单 API 生成流程(10 步)
1. **先读参考文档** --- 必须读 lessons_learned.md、error_debugging_guide.md、complete_example.md
2. 确定 API Request 类名(如 `DescribeAnalysisTimeRequest`)
3. 按分类表归入业务域(如 监控分析 → `monitoring_analysis/`)
4. 读对应 Service 文件,检查是否已有封装方法
5. **读 SDK Parameters 源码**,确认构造函数参数顺序和可用的 setter 方法
6. 读 API 追踪文档,确认当前测试状态(避免重复编写)
7. 按模板 + 规则生成测试文件
8. **运行 pytest 验证**,修复失败用例
9. 更新追踪文档,标记为"已测试"
10. **如遇新问题,自动追加到 lessons_learned.md**
> **实践经验**
>
> **最重要的三步**:步骤 1(先读经验再动手 --- 避免重蹈覆辙)、步骤 5(读 SDK 源码而非凭猜 --- 参数签名不统一是云缓存 SDK 的高频问题)、步骤 10(失败后自动沉淀 --- 让 Skill 越用越聪明)。
### 3.2 场景型批量生成流程(8 步)
当用户提供 **3 个以上共享同一资源名词**的 API(如 AddWhiteListGroup / DeleteWhiteListGroup / ModifyWhiteListGroup),Skill 自动切换为场景模式:
1. 读取全部参考文档,**额外读取 scenario_testing_guide.md**
2. **绘制 API 依赖关系图**:谁创建数据、谁查询、谁修改、谁删除
3. 确定编写顺序:查询 → 创建 → 修改 → 删除(查询 API 是其他 API 的验证手段)
4. **批量**读取所有相关 SDK Parameters 源码,检查 Service 层封装是否匹配
5. 优先修复 Service 层不匹配的地方,再写测试
6. 按四种场景模式生成测试(见第五章)
7. **批量运行所有测试**,检测跨用例干扰
8. 更新追踪文档和踩坑经验
---
## 四、模板设计:统一代码架构
### 4.1 Skill 强制约束的四层架构
生成的测试代码必须遵循项目已有的四层分工:
```
测试用例层 test_cases/cache/test_cases/{业务域}/test_{ApiName}Request.py
↓ 使用 fixture + 实例化 service
Fixture 层 test_cases/cache/utils/fixtures/base_fixtures.py
↓ 提供客户端和测试数据
Service 层 test_cases/cache/utils/services/{业务域}_service.py
↓ 封装 SDK 调用,统一返回 (bool, Any)
SDK 层 cloud_sdk/services/cache/apis/{ApiName}Request.py
```
#### 架构约束如何在 Skill 中落地
| 架构要求 | 在 Skill 中的实现方式 |
|---------|---------------------|
| 测试文件不直接调用 SDK | 模板中 import 只写 Service 类 + 规则"禁止在测试文件中 import SDK Request 类" |
| Fixture 统一管理 | 模板中通过方法参数注入 `setup_clients`, `instance_data` + 规则"禁止在测试文件中定义 fixture" |
| Service 方法与 SDK 参数一致 | 工作流第 5 步强制读 SDK 源码 + 规则"编写 service 方法前必须读 SDK Parameters 源码" |
| Service 统一返回格式 | 模板中写明 `(success: bool, result_or_error: Any)`,所有 service 方法通过基类 `_send_request_with_error_handling` 统一处理 |
| 按业务域分目录 | SKILL.md 中的 API Classification 分类表,工作流第 3 步自动归类 |
> **实践经验**
>
> **同一条架构要求,在 Skill 的三个位置重复强调**:模板里体现正确写法、规则里禁止错误写法、工作流里设置检查步骤。三重保障比只写一次可靠得多。
### 4.2 测试用例的标准模板
模板规定每个测试文件生成以下标准化方法,**按优先级严格排列**:
| 优先级 | 测试类型 | 数量 | 说明 |
|--------|----------|------|------|
| **P0** | 核心功能测试 | 恰好 1 个 | 验证 API 基本功能,三层断言 |
| P1 | 错误处理 / 边界值 / 枚举值 | 多个(含参数化) | 无效实例 ID、最小/最大值、有效/无效枚举 |
| P2 | 参数组合 / 异常值 | 参数化 | null / 类型错误 / 格式异常 / 超长值 |
#### 模板中的三层断言策略
```python
# 第一层:API 调用是否成功
assert success, f"API调用失败:{result}"
# 第二层:响应是否包含预期字段
assert isinstance(result, dict), f"响应应为dict类型,实际: {type(result).__name__}"
assert 'expectedField' in result, f"缺少expectedField字段"
# 第三层:字段值是否符合预期
assert result['expectedField'] == expected_value, f"字段值不符合预期"
```
模板还区分了**查询 API 和写操作 API 的不同断言策略**:
- **查询类**(Describe/List/Get)--- 必须硬断言 `isinstance(result, dict)`,null 就报错
- **写操作类**(Create/Modify/Delete)--- null 返回可能是正常行为,用条件判断
- **写操作 P0** --- 必须调用对应查询 API 交叉验证,不能只信任写操作返回值
### 4.3 Marker 自动匹配
模板根据 API 名称前缀自动选择 pytest marker,无需手动指定:
- `Describe*/List*/Get*` → `@pytest.mark.cache_query`
- `Modify*/Update*` → `@pytest.mark.cache_modify`
- `Create*/Add*` → `@pytest.mark.cache_create`
- `Delete*/Remove*` → `@pytest.mark.cache_delete`
---
## 五、场景型测试设计
云缓存产品中,很多 API 属于同一业务功能的增删改查(如白名单管理、账号管理、配置模板管理),单个 API 逐个测试不够,需要**整体规划 CRUD 测试场景**。
### 5.1 什么时候进入场景模式
Skill 在 SKILL.md 中定义了自动识别规则:当用户提供的 API 满足以下条件时自动切换 ---
- 3 个以上 API
- 共享同一资源名词(如 WhiteListGroup、Account、ConfigTemplate)
### 5.2 四种标准场景模式
| 模式 | 流程 | 云缓存中的实际场景 |
|------|------|-------------------|
| **创建→验证→清理** | 调用 Add → 用 Describe 确认存在 → Delete 清理 | 添加白名单分组、创建账号、创建配置模板 |
| **创建→删除→验证** | 先 Add → Delete → 用 Describe 确认消失 | 删除白名单分组、删除账号 |
| **创建→修改→验证→清理** | Add → Modify → Describe 验证 → Delete | 修改白名单分组 IP、修改账号权限 |
| **保存→修改→验证→恢复** | Describe 保存原值 → Modify → 验证 → 恢复原值 | 修改全局 IP 白名单、修改备份策略、修改 TLS 配置 |
### 5.3 场景型代码示例:创建→验证→清理
```python
def test_add_white_list_group_basic(self, setup_clients, instance_data):
service = SecurityAccessService(setup_clients)
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
service.wait_for_instance_status(cacheInstanceId)
# 步骤1: 添加白名单分组(测试数据名用纯字母+数字,避免命名校验陷阱)
test_name = f"autotestadd{int(time.time()) % 100000}"
success, result = service.add_white_list_group(cacheInstanceId, test_name, ips="10.0.0.1/24")
assert success, f"添加失败: {result}"
# 步骤2: 用查询 API 交叉验证(规则15:不只信任写操作返回值)
service.wait_for_instance_status(cacheInstanceId)
query_success, query_result = service.describe_white_list_group(cacheInstanceId)
names = [g.get('name') for g in query_result['whiteLists']]
assert test_name in names, f"新资源未出现在列表中"
# 步骤3: 清理(规则14:清理失败只 warning,不影响测试判定)
service.wait_for_instance_status(cacheInstanceId)
del_success, del_result = service.delete_white_list_group(cacheInstanceId, test_name)
if not del_success:
logging.warning(f"清理失败: {del_result}")
```
### 5.4 跨 API 业务约束测试
场景型的 P1 测试还覆盖了**跨 API 的业务规则**,这是单 API 测试无法触及的:
| 测试场景 | 预期结果 | 验证的业务规则 |
|---------|---------|--------------|
| 重复创建同名白名单分组 | 第二次应失败 | 分组名唯一性 |
| 删除系统默认分组 default | 应被拒绝 | 系统资源保护 |
| 删除 / 修改不存在的资源 | 应返回错误 | 资源存在性校验 |
### 5.5 实际场景案例
| 场景 | 涉及 API | 覆盖模式 |
|------|----------|---------|
| 白名单管理 | Add / Delete / Modify / Describe WhiteListGroup + ModifyIpWhiteList(5 个) | 全部 4 种模式,共 41 个用例 |
| 账号管理 | Create / Delete / Modify / Describe Account(4 个) | 模式 1/2/3 |
| 配置模板管理 | Create / Delete / Modify / Describe / Clone Template(5 个) | 模式 1/2/3 |
| TLS 配置 | Describe / Modify InstanceTLS(2 个) | 模式 4 |
---
## 六、防错规则体系
### 6.1 两级规则管理
| 级别 | 存放位置 | 加载方式 | 内容形式 |
|------|---------|---------|---------|
| **一级** | SKILL.md → Common Pitfalls to Avoid | 每次触发自动加载 | 一句话禁令(如"禁止 xxx"、"必须 xxx") |
| **二级** | references/lessons_learned.md | 工作流第 1 步主动读取 | 完整案例(现象 → 根因 → 代码对比 → 影响范围) |
#### 规则提升机制
当一个陷阱**被踩 2 次以上**,将核心结论提炼为一句话,提升到 SKILL.md 一级位置。详细案例保留在二级供查阅。当前已提升 4 条:
- 条件分支导致假通过(陷阱 5)
- 已删除实例导致 5 分钟超时(陷阱 6)
- 测试数据命名规则违反 API 校验(陷阱 8)
- SDK Parameters 构造函数参数不统一(陷阱 9)
### 6.2 15 条一级规则一览
**架构规则(7 条)**
1. 禁止用 `self.instance_data`,必须用方法参数 `instance_data`(参数化测试中也必须包含,且位于 parametrize 参数之前)
2. 禁止在测试文件中定义 fixture
3. 禁止用 `@pytest.mark.usefixtures`,用方法参数注入
4. 禁止在测试文件中 import SDK Request 类,通过 Service 层调用
5. 禁止用 `time.sleep()`,用 `service.wait_for_instance_status()`
6. 所有注释和日志必须用中文
7. 文件名必须匹配 SDK Request 类名:`test_{ApiRequestName}.py`
**响应处理规则(3 条)**
8. 查询类 API 必须硬断言 `isinstance(result, dict)`,禁止条件分支静默放过
9. 写操作 API 的 null 返回是正常行为,用条件判断
10. 统一使用 `native_cluster` 存活实例,避免已删除实例导致超时
**Service 层规则(2 条)**
11. 编写 service 方法前**必须读 SDK Parameters 源码**,确认构造函数参数和 setter 方法
12. API 参数引用外部资源时,必须通过对应服务查询接口获取真实数据,禁止用假 ID
**测试数据规则(3 条)**
13. 测试数据名称必须符合 API 参数校验规则(如白名单分组名只允许中文/英文/数字)
14. 写操作必须有清理逻辑,清理失败只 `logging.warning()` 不 fail
15. 写操作 P0 必须用查询 API 交叉验证
> **实践经验**
>
> **用"禁止/必须"而非"建议/推荐"**。实践表明,AI 对绝对约束的遵守率远高于模糊建议。规则中同时给出正面做法和反面做法的代码对比,效果更好。
---
## 七、踩坑经验沉淀
以下是在云缓存测试实践中积累的典型案例。每个案例的价值不仅是解决方案本身,更在于它**如何被转化为 Skill 中的规则或工作流步骤**。
### 7.1 "假通过" --- 测试 PASSED 但什么都没验证
查询 API 有时返回 null,`if/else` 分支的 else 只写了 `logging.info()`,测试静默通过。
**错误做法:**
```python
if isinstance(result, dict):
assert 'tlsStatus' in result
else:
logging.info("跳过验证")
# ← PASSED,什么都没验证!
```
**正确做法:**
```python
assert isinstance(result, dict), \
f"响应应为dict,实际: {type(result)}"
assert 'tlsStatus' in result
# ← null 直接报错,不放水
```
**沉淀方式**:写入 SKILL.md 一级规则第 8 条,同时在 lessons_learned.md 保留完整案例。
### 7.2 SDK Parameters 签名不统一 --- 凭猜写代码必报错
同一系列 API 的 Parameters 类,有些全部通过构造函数传参,有些提供 setter。
```python
# Add 接口:构造函数(regionId, instanceId, name),setIps()可选
parameters = AddParameters(regionId, instanceId, name)
parameters.setIps(ips) # 存在 setter
# Modify 接口:构造函数(regionId, instanceId, ips, name),没有 setter
parameters = ModifyParameters(regionId, instanceId)
parameters.setName(name) # 不存在!AttributeError
```
**沉淀方式**:写入规则第 11 条 + 加入工作流第 5 步(读源码确认签名)。
### 7.3 跨服务资源依赖 --- API 参数引用了别的服务的资源
在编写 `ModifyPublicAddressRequest`(修改实例公网访问配置)的测试用例时,遇到了一个典型的**跨服务资源依赖问题**:该 API 的 allocate(开启公网)操作需要传入 `elasticIpId`(弹性公网 IP ID),但这个资源不属于缓存服务,而是归属 VPC 网络服务。
**错误做法:用假 ID 凑合**
最初编写测试时,直接使用 `"fip-nonexistent12345"` 作为 elasticIpId,导致 allocate 操作永远返回"资源不存在"错误。看似测试通过了,但实际上**只验证了错误路径,核心的"成功开启公网"功能完全没有覆盖**。
**正确做法:主动寻找跨服务查询接口获取真实数据**
解决此问题需要完成以下 5 步系统化查找流程:
1. **从 SDK 参数文件识别需要什么外部资源** --- 读 `ModifyPublicAddressParameters` 源码,发现 `setElasticIpId()` → 需要弹性公网 IP
2. **全局搜索哪些服务有这个资源的 API** --- 在 SDK 中搜索 `ElasticIp` 相关 API,发现 VPC(25 个)、CPS(13 个)、VM(2 个)三个服务都有
3. **判断资源归属服务** --- 看谁有完整 CRUD:VPC 拥有 Create/Describe/Modify/Delete 全套 → VPC 是资源归属方
4. **确认项目中有对应 client 可用** --- 查看配置文件,确认已有 `vpc_client`
5. **在 Service 层封装跨服务调用** --- 用 `vpc_client` 调用 `DescribeElasticIpsRequest` 查询未绑定的 EIP
**错误做法代码:**
```python
# 用假ID,只能测错误路径
eip_id = "fip-nonexistent12345"
success, result = service.modify_public_address(
instanceId, action="allocate",
elasticIpId=eip_id
)
# 永远失败 → 核心功能未验证
```
**正确做法代码:**
```python
# 通过VPC服务查询真实的未绑定EIP
success, result = service.describe_elastic_ips(
status="NOT_ASSOCIATED"
)
eip_id = result['elasticIps'][0]['elasticIpId']
# 用真实EIP测试开启公网
success, result = service.modify_public_address(
instanceId, action="allocate",
elasticIpId=eip_id
)
# 真正验证了核心功能
```
**关键难点:同名 API 出现在多个服务中**
`DescribeElasticIpsRequest` 同时存在于 VPC、CPS、VM 三个服务的 SDK 中。如果凭直觉随便选了 CPS 的版本,由于 SDK 的 `send()` 方法会校验 `service_name`,用 `vpc_client` 发送 CPS 的 Request 会签名失败。必须确保**Request 类和 Client 属于同一个服务**。
**沉淀为 Skill 规则和工作流**
这个案例促成了两个改进:
- **一级规则第 12 条**:"API 参数引用外部资源时,必须通过对应服务查询接口获取真实数据,禁止用假 ID"
- **lessons_learned.md 陷阱 10**:完整记录了 5 步查找方法,供后续遇到类似跨服务依赖时参考
### 7.4 其他高价值案例
| 陷阱 | 现象 | 沉淀到 Skill 的位置 |
|------|------|-------------------|
| 已删除实例超时 | 硬编码实例 ID 已删除,wait 方法卡 5 分钟 | 一级规则第 10 条 |
| 字段名与 API 名不一致 | GetBackupFiles 返回 `backups` 而非 `backupFiles` | 最佳实践(先打印再断言) |
| 时间参数格式差异 | 不同 API 要求 RFC3339 / datetime / 时间戳 | 经验库中的格式对照表 |
| 测试数据命名被拒绝 | 含下划线的名称被 API 参数校验拒绝 | 一级规则第 13 条 |
| 诊断任务 ≠ 常规任务 | 两套独立系统,用错查询 API 永远找不到 | 经验库陷阱 1 + 排查清单 |
| 慢日志时间范围限制 | 7 天范围返回 400,文档未说明 | 经验库陷阱 7 |
| 跨服务资源引用 | API 参数需要 VPC 服务的 elasticIpId,用假 ID 只能测错误路径 | 一级规则第 12 条 + 5 步查找方法 |
| 写操作返回成功 ≠ 真的成功 | 创建操作返回成功,但查询发现资源未创建 | 一级规则第 15 条 |
> **实践经验**
>
> 经验库中每个条目遵循统一格式:**问题描述 → 实际案例 → 解决方案 → 代码对比**。同时在 lessons_learned.md 末尾维护一份**排查清单**,遇到问题时按清单逐项排查。
---
## 八、Skill 构建方法总结
### 8.1 迭代路径
| 阶段 | 做什么 | 产出 |
|------|--------|------|
| **V0** | 一个工作流 + 一个基础模板 | 能生成"结构正确"的代码 |
| **V1** | 把前几次踩的坑写成规则 | 代码结构正确 + 避开已知陷阱 |
| **V2** | 建立 references/ 目录,积累详细案例和排查指南 | AI 能理解"为什么",而不只是"怎么做" |
| **V3** | 加入场景模式,支持批量 CRUD 闭环 | 从单点生成进化为整体测试规划 |
### 8.2 核心经验
| 经验 | 在云缓存实践中的具体体现 |
|------|------------------------|
| **Skill = 工作流 + 模板 + 规则 + 经验** | 10 步工作流 + pytest 代码模板 + 15 条规则 + 10 个踩坑案例,缺一不可 |
| **规则要分级管理** | 高频规则放 SKILL.md 自动加载,详细案例放 references/ 按需读取 |
| **Skill 要能自我进化** | 工作流末尾"遇到新问题自动追加"+ 提升机制(踩 2 次升一级) |
| **架构约束要三重保障** | 四层架构在模板中体现、在规则中禁止反面、在工作流中设检查步骤 |
| **场景模式是质的飞跃** | 白名单 5 个 API 产出 41 个用例,覆盖 4 种 CRUD 模式 + 跨 API 业务约束 |
| **"禁止"比"建议"有效** | AI 对绝对约束遵守率远高于模糊建议,规则要配代码对比 |
### 8.3 可迁移到其他产品的设计模式
- **两级规则管理 + 提升机制** --- 适用于任何需要持续积累领域经验的 Skill
- **工作流末尾的自动经验沉淀** --- 适用于产出物需要持续改进的场景
- **三层断言策略 + 查询/写操作差异化** --- 适用于所有 API 测试生成类 Skill
- **场景识别 + 自动切换模式** --- 适用于有"单点 vs 批量"两种工作模式的场景
- **SDK 源码驱动的参数确认** --- 适用于 SDK 接口不统一的云服务产品