接口自动化踩坑经验skill

0 阅读9分钟
# Redis API 测试经验教训

本文档记录编写 Redis API 测试用例的**踩坑经验与详细案例**。生成测试用例前**必须先查阅此文档**。

> 遇到新的踩坑经验,直接追加到本文档对应分类下。

## 📌 关于高频规则的两级管理

| 规则类型 | 放哪里 | 原因 |
|---------|--------|------|
| **高频/每次都要遵守的规则** | `SKILL.md` → Common Pitfalls to Avoid | 每次触发skill自动加载,注意力权重高 |
| **踩坑的详细案例和背景** | 本文件 `lessons_learned.md` | 需要理解"为什么"时查阅,保留完整上下文 |

**当一个陷阱被踩了2次以上**,应该将其核心规则提升到 `SKILL.md` 的 Common Pitfalls 中(一句话规则),本文件保留详细案例。
当前已提升到SKILL.md的陷阱:陷阱5(假通过)、陷阱6(已删除实例)、陷阱8(命名规则)、陷阱9(SDK参数不一致)。

---

## 常见陷阱与解决方案

### 陷阱1:诊断任务与常规任务混用查询API

**问题描述**:诊断任务和常规任务是两个独立的系统,使用不同的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" 的通常是诊断任务
- 常规运维操作(变配、删除、重启等)是常规任务

---

### 陷阱2:API响应字段名与预期不符

**问题描述**: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())}")
    # 然后根据实际字段调整断言
```

---

### 陷阱3:时间参数格式要求

**问题描述**:不同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

# RFC3339格式(UTC时间)
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')

# 普通datetime格式
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错误时,首先检查时间格式

---

### 陷阱4:空值处理

**问题描述**:API可能返回 `null` 而不是空列表 `[]`

**实际案例**:`GetBackupFilesRequest` 在没有备份时返回 `backups: null`

**解决方案**:

```python
backups = result.get('backups')
if backups is not None:
    assert isinstance(backups, list)
    # 处理列表逻辑
else:
    logging.info("返回null,表示没有数据")
```

---

### 陷阱5:`if isinstance(result, dict) ... else: logging.info()` 导致假通过

**问题描述**:当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的场景下,才应该用条件判断而非硬断言。

---

### 陷阱6:已删除实例导致 `wait_for_instance_status` 无限重试超时

**问题描述**:`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']  # 可能已删除

# 正确做法:统一使用存活的native_cluster实例,通过不同验证维度区分测试
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
```

---

## 最佳实践

### 实践1:首次编写测试用例的调试流程

当编写新接口的测试用例时,建议按以下顺序调试:

```python
# 步骤1: 先运行最基本的调用,查看是否能成功
success, result = service.some_method(cacheInstanceId)
logging.info(f"API调用结果: success={success}, result={result}")

# 步骤2: 查看实际返回的字段结构
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__})")

# 步骤3: 根据实际返回调整断言
assert '实际字段名' in result, f"响应中缺少字段,实际: {result}"
```

---

### 实践2:时间参数的处理策略

对于包含时间参数的API,建议:

1. **先不带时间参数测试基本功能**(P0测试)
2. **单独测试时间参数**(P1测试),失败时检查格式
3. **常见时间格式尝试顺序**:
   - RFC3339格式: `%Y-%m-%dT%H:%M:%SZ`
   - 普通格式: `%Y-%m-%d %H:%M:%S`
   - Unix毫秒时间戳

---

### 实践3:断言的粒度控制

```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)

---

### 陷阱7:慢日志类API时间范围限制

**问题描述**:`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天/303. 将超出范围的时间作为异常参数测试(预期返回错误)

```python
# 正确做法:使用1天以内的时间范围
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')
```

---

### 陷阱8:白名单分组名命名规则限制

**问题描述**:`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}"
```

---

### 陷阱9:白名单分组API的service层参数与SDK不一致

**问题描述**:白名单分组相关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)