本文档记录编写 Redis API 测试用例的**踩坑经验与详细案例**。生成测试用例前**必须先查阅此文档**。
> 遇到新的踩坑经验,直接追加到本文档对应分类下。
| 规则类型 | 放哪里 | 原因 |
|---------|--------|------|
| **高频/每次都要遵守的规则** | `SKILL.md` → Common Pitfalls to Avoid | 每次触发skill自动加载,注意力权重高 |
| **踩坑的详细案例和背景** | 本文件 `lessons_learned.md` | 需要理解"为什么"时查阅,保留完整上下文 |
**当一个陷阱被踩了2次以上**,应该将其核心规则提升到 `SKILL.md` 的 Common Pitfalls 中(一句话规则),本文件保留详细案例。
当前已提升到SKILL.md的陷阱:陷阱5(假通过)、陷阱6(已删除实例)、陷阱8(命名规则)、陷阱9(SDK参数不一致)。
---
**问题描述**:诊断任务和常规任务是两个独立的系统,使用不同的API查询
**实际案例**:`UserDiagnoseInstancesRequest` 测试失败
- 错误做法:创建诊断任务后,使用 `wait_for_task_success()` 等待(查询常规任务列表)
- 结果:查询任务列表为空,等待超时
- 根本原因:诊断任务不在常规任务列表中
**关键区别**:
| 任务类型 | 查询API | 响应结构 | 示例 |
|---------|---------|---------|------|
| 诊断任务 | `ListInstanceDiagnoseTasksRequest` | `{'total': N, 'result': [...]}` | 实例诊断、健康检查 |
| 常规任务 | `ListTasksRequest` | `{'tasks': [...], 'totalCount': N}` | 变配、删除、启动、停止 |
**解决方案**:
```python
wait_success, wait_result = task_management_service.wait_for_diagnose_task_success(
cacheInstanceId, timeout=300, interval=2
)
wait_success, wait_result = task_management_service.wait_for_task_success(
instanceId, timeout=300, interval=2
)
```
**识别方法**:
- API名称包含 "Diagnose"、"Diagnosis" 的通常是诊断任务
- 常规运维操作(变配、删除、重启等)是常规任务
---
**问题描述**:SDK文档或API名称暗示的字段名可能与实际API返回不一致
**实际案例**:`GetBackupFilesRequest`
- 预期字段:`backupFiles`, `totalCount`(根据API名称推断)
- 实际字段:`backups`, `count`(实际API返回)
**解决方案**:
1. 首次编写测试时,先用宽松断言验证响应结构
2. 查看实际API返回的JSON字段名
3. 根据实际响应调整断言
```python
if isinstance(result, dict):
logging.info(f"实际返回字段: {list(result.keys())}")
```
---
**问题描述**:不同API对时间格式要求不同,使用错误格式会导致400错误
**常见时间格式**:
| 格式类型 | 示例 | 适用API |
|---------|------|---------|
| RFC3339 | `2024-01-01T00:00:00Z` | GetMetricRequest, GetBackupFilesRequest |
| 普通datetime | `2024-01-01 00:00:00` | DescribeBackupsRequest |
| Unix时间戳 | `1704067200` | 部分监控类API |
**解决方案**:
```python
import datetime
end_time = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
start_time = (datetime.datetime.utcnow() - datetime.timedelta(days=30)).strftime('%Y-%m-%dT%H:%M:%SZ')
end_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
start_time = (datetime.datetime.now() - datetime.timedelta(days=30)).strftime('%Y-%m-%d %H:%M:%S')
```
**调试建议**:当时间参数测试返回400错误时,首先检查时间格式
---
**问题描述**:API可能返回 `null` 而不是空列表 `[]`
**实际案例**:`GetBackupFilesRequest` 在没有备份时返回 `backups: null`
**解决方案**:
```python
backups = result.get('backups')
if backups is not None:
assert isinstance(backups, list)
else:
logging.info("返回null,表示没有数据")
```
---
**问题描述**:当API返回null时,service层返回的是SDK response对象而非dict。如果用 `if isinstance(result, dict)` 分支做断言,`else` 分支只写 `logging.info()` 就放过,会导致测试在API返回异常时静默通过,完全没有验证任何东西。
**实际案例**:`DescribeInstanceTLSRequest` 测试
- 错误做法:
```python
if isinstance(result, dict):
assert 'tlsStatus' in result
else:
logging.info("API返回null,跳过验证")
```
- 结果:如果API返回null,测试直接PASSED,但实际什么都没验证
**解决方案**:对于查询类API,响应必须是dict才算正常,直接用 `assert isinstance` 硬断言:
```python
assert isinstance(result, dict), f"响应应为dict类型,实际: {type(result).__name__}"
assert 'tlsStatus' in result, f"缺少tlsStatus字段,实际: {result}"
```
**适用场景**:所有查询类API(Describe*、List*、Get*)的响应验证。只有在API文档明确说明可能返回null的场景下,才应该用条件判断而非硬断言。
---
**问题描述**:`base_fixtures.py` 中硬编码的实例ID(如standard、proxy_cluster对应的默认ID)可能已被删除。`wait_for_instance_status` 对已删除实例会一直重试查询直到超时(默认300秒),严重浪费测试执行时间。
**实际案例**:`DescribeInstanceTLSRequest` 测试
- 实例 `redis-24rkbytg7pat`(standard和proxy_cluster默认ID)已被删除
- `wait_for_instance_status` 每10秒重试一次,每次返回404
- 测试卡住约5分钟才超时失败
**解决方案**:
1. 测试用例优先使用 `native_cluster` 实例(当前存活的实例)
2. 如果需要测试多实例类型,不要硬依赖standard/proxy_cluster的默认ID
3. 可以改为测试不同维度的验证(如字段类型、字段完整性、证书字段等)代替多实例类型覆盖
```python
cacheInstanceId = instance_data['standard']['cacheInstanceId']
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
```
---
当编写新接口的测试用例时,建议按以下顺序调试:
```python
success, result = service.some_method(cacheInstanceId)
logging.info(f"API调用结果: success={success}, result={result}")
if isinstance(result, dict):
logging.info(f"返回字段: {list(result.keys())}")
for key, value in result.items():
logging.info(f"{key}: {value} (type: {type(value).__name__})")
assert '实际字段名' in result, f"响应中缺少字段,实际: {result}"
```
---
对于包含时间参数的API,建议:
1. **先不带时间参数测试基本功能**(P0测试)
2. **单独测试时间参数**(P1测试),失败时检查格式
3. **常见时间格式尝试顺序**:
- RFC3339格式: `%Y-%m-%dT%H:%M:%SZ`
- 普通格式: `%Y-%m-%d %H:%M:%S`
- Unix毫秒时间戳
---
```python
assert result == {'backupFiles': [], 'totalCount': 0}
assert 'backups' in result, f"缺少backups字段,实际: {result}"
assert 'count' in result, f"缺少count字段,实际: {result}"
if result['backups'] is not None:
assert isinstance(result['backups'], list)
```
---
编写新接口测试时,遇到问题的排查顺序:
- [ ] 确认任务类型(诊断任务 vs 常规任务)
- [ ] API调用是否成功(success=True)
- [ ] 查看实际返回的字段名(不要靠猜)
- [ ] 检查时间参数格式(如返回400错误)
- [ ] 处理可能的null值
- [ ] 验证分页参数边界值
- [ ] 测试无效参数的错误处理
- [ ] 断言不能有静默放过的else分支(陷阱5)
- [ ] 确认使用的实例ID是否仍然存活(陷阱6)
- [ ] 慢日志类API时间范围限制(陷阱7)
- [ ] 白名单分组名只允许中文、英文、数字(陷阱8)
- [ ] SDK Parameters类的构造函数参数和setter方法(陷阱9)
---
**问题描述**:`DescribeProxySlowLogRequest` 和类似的慢日志查询API对时间范围有严格限制,超出范围会返回400错误。
**实际案例**:`DescribeProxySlowLogRequest` 测试
- 使用7天时间范围查询,API返回 `INVALID_ARGUMENT: invalid time`
- 使用30天以上时间范围查询,API返回 `startTime start from 30 days ago at most`
- 使用1天时间范围查询成功
**解决方案**:
1. 慢日志类API的时间范围测试,默认使用1天以内的范围
2. 时间边界测试参数化时,使用小范围(6小时、1天),不要用7天/30天
3. 将超出范围的时间作为异常参数测试(预期返回错误)
```python
end_time = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
start_time = (datetime.datetime.utcnow() - datetime.timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%SZ')
start_time = (datetime.datetime.utcnow() - datetime.timedelta(days=7)).strftime('%Y-%m-%dT%H:%M:%SZ')
```
---
**问题描述**:`AddWhiteListGroupRequest` 等白名单分组API对分组名有严格的命名规则限制,只允许中文、英文字母和数字,不允许下划线、连字符、特殊字符等。
**实际案例**:`AddWhiteListGroupRequest` 测试
- 使用 `autotest_add_12345` 格式的名称(含下划线),API返回 `INVALID_ARGUMENT: name must be 1-64 characters of Chinese, English, and numbers`
- 使用 `autotestadd12345` 格式的名称(纯字母+数字),API调用成功
**解决方案**:
1. 白名单分组名只能使用中文、英文字母和数字,长度1-64字符
2. 测试用例中的自动生成分组名使用纯字母+数字格式,不要用下划线或连字符
```python
test_group_name = f"autotest_add_{int(time.time()) % 100000}"
test_group_name = f"autotestadd{int(time.time()) % 100000}"
```
---
**问题描述**:白名单分组相关API的SDK Parameters类使用的参数名与service层封装时假设的setter方法不一致。SDK使用构造函数参数和直接属性,不提供setter方法。
**实际案例**:
- `AddWhiteListGroupParameters` 构造函数接收 `(regionId, cacheInstanceId, name)`,可选方法为 `setIps(ips)`
- service层错误地调用了 `setGroupName()`, `setIpList()`, `setGroupType()` 等不存在的方法
- `ModifyWhiteListGroupParameters` 构造函数接收 `(regionId, cacheInstanceId, ips, name)` 四个必填参数
**解决方案**:编写service方法前必须先读取SDK的Parameters类源码,确认构造函数参数和可用的setter方法。
---
- [Skill 主文档 - SKILL.md](../SKILL.md)