核心功能
-
统一测试架构:采用三层架构设计(测试用例层 → Fixture层 → Service层 → SDK层),实现职责分离和代码复用
-
规范测试编写流程:
- 指定文件命名规则和目录结构
- 定义测试优先级(P0-P3)及编写顺序
- 提供完整的测试方法模板
-
断言设计规范:
- 三层断言策略:调用状态 → 响应结构 → 字段值
- 强制要求断言信息包含上下文
- 提供多种场景的断言模板(基本功能、边界值、错误处理、列表/分页响应)
-
API分类管理:
- 实例管理、备份恢复、监控分析、安全访问、配置管理、任务管理
- 每类API对应独立的Service类和文件夹
关键规范
| 规范项 | 要求 |
|---|---|
| 响应处理 | 使用 isinstance(result, dict) 区分空响应和正常响应 |
| 优先级标记 | 必须使用 @pytest.mark.priority 装饰器 |
| 编写顺序 | P0 → P1 → P2 → P3 |
| 断言信息 | 必须包含上下文(参数值、实际结果) |
# API测试用例生成模板 (重构版)
## 模板说明
此模板用于生成Redis API测试用例脚本。根据API文档和需求,按照此模板结构生成完整的测试脚本,确保覆盖API的主要功能点、边界条件和异常情况。需要为**未测试**的接口编写测试用例,即那些在`redis_apis_summary.md`文件中没有标记为[已测试]的接口。按照文件中顺序依次编写。
## 重要提示(必读)
1. **🚨 响应处理关键规范**:当API返回`{"result": null}`时,必须使用`isinstance(result, dict)`进行类型检查,区分处理字典响应和SDK响应对象。
```python
success, result = service.some_api_method(params)
assert success, f"API调用失败:{result}"
if isinstance(result, dict):
assert 'expectedField' in result, "响应中缺少预期字段"
else:
logging.info("API返回空结果(null),这是正常情况")
```
2. **使用服务层架构**:不再直接调用SDK的Request类,而是通过对应的service类调用API方法。每个service类都继承自BaseRedisService,提供统一的错误处理、日志记录和实例状态等待功能。
3. **自动等待实例状态**:service层的`wait_for_instance_status`方法会自动等待实例状态变为"running",不需要手动调用`time.sleep`。
4. **编写测试用例后必须进行运行验证**,确保代码能够正常执行。
5. **完成测试后必须更新`redis_apis_summary.md`文件**,将测试的API标记为[已测试],必要时添加注释说明特殊情况或注意事项。
## 框架架构设计
采用三层架构设计,实现清晰的职责分离:
```
测试用例层 (Test Case Layer)
↓ 使用基础fixture + 直接实例化service
Fixture层 (Fixture Layer) - 仅基础资源
↓ 提供基础配置
Service层 (Service Layer)
↓ 封装调用
SDK层 (SDK Layer)
```
### 各层职责
| 层级 | 职责 | 位置 |
|------|------|------|
| 测试用例层 | 测试逻辑 + Service层实例化 | `test_cases/redis/test_cases/` |
| Fixture层 | 管理基础测试资源(客户端配置、测试数据) | `test_cases/redis/utils/fixtures/` |
| Service层 | 封装业务逻辑和API调用 | `test_cases/redis/utils/services/` |
| SDK层 | 提供底层API接口 | `jdcloud_sdk/services/redis/` |
### 架构优势
- **职责单一**:每层都有明确的职责边界
- **高度复用**:Fixture层和Service层可在多个测试中复用
- **易于维护**:修改某层不会影响其他层
- **测试隔离**:通过Fixture层确保测试之间的资源隔离
## API分类与文件组织
### 文件夹结构
```
test_cases/redis/test_cases/
├── instance_management/ # 实例管理类API
├── backup_recovery/ # 备份与恢复类API
├── monitoring_analysis/ # 监控与分析类API
├── security_access/ # 安全与访问控制类API
├── config_management/ # 配置管理类API
└── task_management/ # 任务管理类API
```
### API分类速查表
| API分类 | 文件夹 | Service类 | 示例API |
|---------|--------|-----------|---------|
| 实例管理类 | `instance_management/` | `InstanceManagementService` | DescribeInstanceNames, CreateCacheInstance, DeleteCacheInstance |
| 备份与恢复类 | `backup_recovery/` | `BackupRecoveryService` | CreateBackup, RestoreInstance, DescribeBackups |
| 监控与分析类 | `monitoring_analysis/` | `MonitoringAnalysisService` | DescribeSlowLog, CreateBigKeyAnalysis, DescribeHotKeySummary |
| 安全与访问控制类 | `security_access/` | `SecurityAccessService` | DescribeIpWhiteList, CreateAccount, ModifyInstanceTLS |
| 配置管理类 | `config_management/` | `ConfigManagementService` | DescribeInstanceConfig, SetDisableCommands |
| 任务管理类 | `task_management/` | `TaskManagementService` | ListTasks, CancelTask |
### 文件命名规则
1. 文件名以 "test_" 开头
2. 接着是被测试的 API 名称,与 SDK 中的 Request 类名保持一致
3. 文件扩展名为 ".py"
4. 文件必须放在对应的分类文件夹中
**示例**:`CheckDeletableRequest` API → `test_cases/redis/test_cases/instance_management/test_CheckDeletableRequest.py`
## 重要路径说明
| 层级 | 路径 | 说明 |
|------|------|------|
| 测试用例 | `test_cases/redis/test_cases/` | 存放所有测试用例文件 |
| 基础Fixture | `test_cases/redis/utils/fixtures/base_fixtures.py` | 客户端配置、测试数据管理 |
| 主配置文件 | `test_cases/redis/conftest.py` | 统一的pytest配置和fixture导入 |
| 基础服务类 | `test_cases/redis/utils/services/base_redis_service.py` | 提供通用功能的基类 |
| 实例管理服务 | `test_cases/redis/utils/services/instance_management_service.py` | 实例管理相关API封装 |
| 监控分析服务 | `test_cases/redis/utils/services/monitoring_analysis_service.py` | 监控分析相关API封装 |
| SDK接口 | `jdcloud_sdk/services/redis/apis/` | API请求类和参数类 |
| SDK模型 | `jdcloud_sdk/services/redis/models/` | API响应数据模型类 |
| 统计文档 | `test_cases/redis/redis_apis_summary.md` | 接口测试状态统计 |
## 测试用例优先级
测试用例必须使用 `@pytest.mark.priority` 装饰器标记优先级:
| 优先级 | 说明 | 示例 |
|--------|------|------|
| **P0** | 核心功能测试,每个接口只能有一个P0用例 | 基本API功能验证 |
| **P1** | 重要功能测试,影响主要功能 | 重要参数验证、边界条件、错误处理 |
| **P2** | 一般功能测试,完善功能覆盖 | 可选参数、一般边界条件 |
| **P3** | 辅助功能测试,增强覆盖率 | 极端边界值、非关键错误处理 |
**编写顺序要求**:必须严格按照 P0 → P1 → P2 → P3 的顺序编写测试用例。
**执行顺序分配**:P0用order=1,P1用order=10-19,P2用order=20-29,P3用order=30-39。
## Fixture使用说明
**重要**:新架构下不再在测试用例中定义fixture,也无需使用`@pytest.mark.usefixtures`装饰器!
### 基础Fixture列表
| Fixture名称 | 说明 |
|-------------|------|
| `setup_clients` | 客户端连接配置 |
| `instance_data` | 测试数据管理(支持命令行参数 `--cacheInstanceId`) |
| `test_environment_config` | 环境配置 |
### 使用方式
```python
class TestExample:
def test_example(self, setup_clients, instance_data):
# 直接创建service实例
service = MonitoringAnalysisService(setup_clients)
# 获取实例ID
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
# 测试逻辑...
```
## 断言设计规范
### 断言分层策略(必读)
断言必须按照以下三层顺序进行,**不可跳跃**:
```
第一层:调用状态断言 → 断言 success 是否符合预期
第二层:响应结构断言 → 断言 result 类型、关键字段是否存在
第三层:字段值断言 → 断言具体字段值是否符合预期
```
### 断言信息编写规范
**必须遵循"三要素"原则**:`断言条件, 错误信息`
```python
# ❌ 错误示例:断言信息不明确
assert success
assert result is not None
assert 'instanceId' in result
# ✅ 正确示例:断言信息包含上下文
assert success, f"API调用失败,实例ID: {cacheInstanceId}, 错误: {result}"
assert isinstance(result, dict), f"响应类型错误,期望dict,实际: {type(result).__name__}"
assert 'instanceId' in result, f"响应中缺少instanceId字段,实际响应: {result}"
```
### 各场景断言模板
#### 1. 基本功能测试断言
```python
# 第一层:调用状态
assert success, f"API调用失败,参数: {params}, 错误: {result}"
# 第二层:响应结构
if isinstance(result, dict):
# 第三层:关键字段
assert 'instanceId' in result, f"响应缺少instanceId字段: {result}"
# 第三层:字段值验证
instance_id = result['instanceId']
assert instance_id == expected_id, f"实例ID不匹配,期望: {expected_id}, 实际: {instance_id}"
else:
logging.info("API返回空结果(null),业务允许的空值")
```
#### 2. 边界值测试断言
```python
# 根据预期结果类型进行断言
if expected == "success":
# 期望成功
assert success, f"边界值 {test_param} 期望成功但失败,错误: {result}"
if isinstance(result, dict):
logging.info(f"边界值 {test_param} 验证成功,响应: {result}")
elif expected == "invalid_param":
# 期望参数错误
assert not success, f"边界值 {test_param} 期望失败但成功了,响应: {result}"
# 可选:验证错误信息
logging.info(f"边界值 {test_param} 正确返回错误: {result}")
else:
# 其他预期类型
assert success == (expected == "success"), f"断言逻辑错误,expected={expected}"
```
#### 3. 错误处理测试断言
```python
# 期望失败
assert not success, f"使用无效参数 {invalid_param} 应该失败但成功了"
# 可选:验证错误信息内容
if isinstance(result, str):
assert "不存在" in result or "not found" in result.lower(), f"错误信息不符合预期: {result}"
logging.info(f"正确返回错误信息: {result}")
```
#### 4. 列表/数组响应断言
```python
# 第一层
assert success, f"查询列表失败: {result}"
# 第二层
if isinstance(result, dict):
# 列表字段存在性
assert 'items' in result or 'data' in result, f"响应缺少列表字段: {result}"
items = result.get('items') or result.get('data', [])
assert isinstance(items, list), f"列表字段类型错误,期望list,实际: {type(items).__name__}"
# 第三层:列表内容
if len(items) > 0:
first_item = items[0]
assert 'id' in first_item, f"列表项缺少id字段: {first_item}"
logging.info(f"列表包含 {len(items)} 条记录")
```
#### 5. 分页响应断言
```python
# 第一层
assert success, f"分页查询失败: {result}"
# 第二层 + 第三层
if isinstance(result, dict):
# 分页字段验证
assert 'totalCount' in result, f"响应缺少totalCount字段"
assert 'pageNumber' in result, f"响应缺少pageNumber字段"
assert 'pageSize' in result, f"响应缺少pageSize字段"
total = result['totalCount']
page = result['pageNumber']
size = result['pageSize']
# 分页逻辑验证
assert total >= 0, f"totalCount不能为负数: {total}"
assert page >= 1, f"pageNumber应该从1开始: {page}"
assert size >= 1, f"pageSize应该大于0: {size}"
logging.info(f"分页验证通过: 第{page}页, 每页{size}条, 共{total}条")
```
### 常见断言错误及修正
| 错误示例 | 问题 | 正确示例 |
|----------|------|----------|
| `assert result` | null响应会失败 | `assert success, f"调用失败: {result}"` |
| `assert 'field' in result` | 未处理非字典情况 | `if isinstance(result, dict): assert 'field' in result` |
| `assert len(result) > 0` | 未区分空列表和null | `if isinstance(result, dict): assert len(result.get('items', [])) >= 0` |
| `assert result['id'] == 1` | 未检查字段存在性 | `assert result.get('id') == 1, f"id不匹配: {result}"` |
| `assert success` | 错误信息缺失 | `assert success, f"调用失败: {result}"` |
### 断言检查清单
编写断言时,请检查以下几点:
- [ ] 是否包含断言信息(第二参数)?
- [ ] 断言信息是否包含关键上下文(参数值、实际结果)?
- [ ] 是否按分层顺序进行(状态→结构→值)?
- [ ] 是否处理了 null/空响应的情况?
- [ ] 边界值测试是否区分了成功/失败的预期?
- [ ] 列表响应是否验证了类型和内容?
## 测试方法模板
### 通用测试方法结构
```python
@pytest.mark.run(order=[执行顺序])
@pytest.mark.redis_[测试类型]
@pytest.mark.priority("[优先级]")
def test_[API名称]_[测试类型](self, setup_clients, instance_data, [其他参数]):
"""
[API功能][测试类型]测试
测试步骤:
1. 检查测试前置条件
2. 创建服务层实例
3. 等待实例状态为running
4. 调用service层API方法
5. 分层断言验证结果
预期结果:[预期结果描述]
"""
# 步骤1: 检查前置条件
if not instance_data['native_cluster']['cacheInstanceId']:
pytest.skip("未提供缓存实例ID,跳过测试")
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
# 步骤2: 创建服务层实例
service = [ServiceClass](setup_clients)
# 步骤3: 等待实例状态
service.wait_for_instance_status(cacheInstanceId)
# 步骤4: 调用API
success, result = service.[api_method_name](cacheInstanceId, [其他参数])
# 步骤5: 分层断言(参考断言设计规范)
# 第一层:调用状态
assert success, f"API调用失败,实例ID: {cacheInstanceId}, 错误: {result}"
# 第二层:响应结构 + 第三层:字段值
if isinstance(result, dict):
assert '[预期字段]' in result, f"响应缺少预期字段,实际响应: {result}"
logging.info("测试通过")
```
### 1. 基本功能测试(P0)
```python
@pytest.mark.run(order=1)
@pytest.mark.redis_query
@pytest.mark.priority("P0")
def test_[API名称]_basic(self, setup_clients, instance_data):
"""[API功能]基本功能测试"""
if not instance_data['native_cluster']['cacheInstanceId']:
pytest.skip("未提供缓存实例ID,跳过测试")
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
service = [ServiceClass](setup_clients)
service.wait_for_instance_status(cacheInstanceId)
success, result = service.[api_method_name](cacheInstanceId)
# 分层断言
assert success, f"API调用失败,实例ID: {cacheInstanceId}, 错误: {result}"
if isinstance(result, dict):
assert '[预期字段]' in result, f"响应缺少预期字段,实际响应: {result}"
logging.info(f"基本功能测试通过,响应包含字段: {list(result.keys())}")
else:
logging.info("API返回空结果(null)")
```
### 2. 参数化测试(P1)
```python
@pytest.mark.run(order=10)
@pytest.mark.redis_query
@pytest.mark.priority("P1")
@pytest.mark.parametrize("param1,param2", [
("value1_1", "value1_2"),
("value2_1", "value2_2"),
], ids=["测试ID1", "测试ID2"])
def test_[API名称]_parametrized(self, setup_clients, instance_data, param1, param2):
"""[API功能]参数化测试"""
if not instance_data['native_cluster']['cacheInstanceId']:
pytest.skip("未提供缓存实例ID,跳过测试")
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
service = [ServiceClass](setup_clients)
service.wait_for_instance_status(cacheInstanceId)
success, result = service.[api_method_name](cacheInstanceId, param1, param2)
# 分层断言
assert success, f"API调用失败,参数: param1={param1}, param2={param2}, 错误: {result}"
if isinstance(result, dict):
assert '[预期字段]' in result, f"响应缺少预期字段,参数: ({param1}, {param2})"
logging.info(f"参数化测试通过 - param1={param1}, param2={param2}")
```
### 3. 边界值测试(P1)
```python
@pytest.mark.run(order=11)
@pytest.mark.redis_query
@pytest.mark.priority("P1")
@pytest.mark.parametrize("test_param,expected", [
(1, "success"), # 最小正值
(0, "invalid_param"), # 零值
(-1, "invalid_param"), # 负值
(100, "success"), # 最大正常值
(101, "param_too_large"), # 超出最大值
], ids=["min_positive", "zero", "negative", "max_normal", "exceed_max"])
def test_[API名称]_boundary(self, setup_clients, instance_data, test_param, expected):
"""[API功能]边界值测试"""
if not instance_data['native_cluster']['cacheInstanceId']:
pytest.skip("未提供缓存实例ID,跳过测试")
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
service = [ServiceClass](setup_clients)
service.wait_for_instance_status(cacheInstanceId)
success, result = service.[api_method_name](cacheInstanceId, test_param)
# 根据预期结果进行断言
if expected == "success":
assert success, f"边界值 {test_param} 期望成功但失败,错误: {result}"
if isinstance(result, dict):
logging.info(f"边界值 {test_param} 验证成功")
else:
assert not success, f"边界值 {test_param} 期望失败但成功了,响应: {result}"
logging.info(f"边界值 {test_param} 正确返回错误,预期: {expected}")
```
### 4. 错误处理测试(P1)
```python
@pytest.mark.run(order=12)
@pytest.mark.redis_query
@pytest.mark.priority("P1")
def test_[API名称]_error_handling(self, setup_clients, instance_data):
"""[API功能]错误处理测试"""
service = [ServiceClass](setup_clients)
# 使用无效参数
invalid_id = "redis-nonexistent-instance"
success, result = service.[api_method_name](invalid_id)
# 断言期望失败
assert not success, f"使用无效实例ID {invalid_id} 应该失败但成功了"
logging.info(f"错误处理测试通过,错误信息: {result}")
```
## 导入部分模板
```python
# -*- coding: utf-8 -*-
# 开发人员 :[开发者姓名]
# 开发时间 :[开发日期]
# 文件名称 :test_[API名称].py
import pytest
import logging
# 根据API分类选择对应的Service类(参考API分类速查表)
from test_cases.redis.utils.services.instance_management_service import InstanceManagementService
# from test_cases.redis.utils.services.backup_recovery_service import BackupRecoveryService
# from test_cases.redis.utils.services.monitoring_analysis_service import MonitoringAnalysisService
```
## 测试验证步骤
```bash
# 激活虚拟环境
source .venv/bin/activate
# 运行单个测试文件
pytest test_cases/redis/test_cases/[分类]/test_[API名称].py -v -s
# 运行单个测试文件(提供缓存实例ID)
pytest test_cases/redis/test_cases/[分类]/test_[API名称].py -v -s --cacheInstanceId=xxxxx
```
## 常见错误及解决方案
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| 响应不是字典格式 | API返回`{"result": null}`时返回SDK对象 | 使用`isinstance(result, dict)`检查 |
| 参数边界值错误 | 参数值超出API允许范围 | 仔细阅读API文档,使用参数化测试覆盖边界值 |
| 响应中缺少字段 | API文档与实际响应不符 | 先打印完整响应分析结构 |
| 依赖资源不存在 | 测试依赖的资源不存在 | 检查资源是否存在,不存在则跳过测试 |
| 认证失败 | API调用返回认证错误 | 检查认证信息是否正确、过期 |
## 最佳实践
1. **增量测试**:先测试基本功能,再测试边界条件和异常情况
2. **详细日志**:记录每个测试步骤、API请求和响应
3. **分层断言**:严格按照 调用状态 → 响应结构 → 字段值 的顺序断言
4. **断言信息**:必须包含上下文信息(参数值、实际结果),便于定位问题
5. **参数化测试**:使用`pytest.mark.parametrize`覆盖正常值、边界值和无效值
6. **资源管理**:使用fixture管理测试资源生命周期,测试结束后清理资源
## 完整测试用例示例
```python
# -*- coding: utf-8 -*-
# 开发人员 :Bie Yuhang
# 开发时间 :2024/11/24
# 文件名称 :test_DescribeInstanceNamesRequest.py
import pytest
import logging
from test_cases.redis.utils.services.instance_management_service import InstanceManagementService
class TestDescribeInstanceNames:
@pytest.mark.run(order=1)
@pytest.mark.redis_query
@pytest.mark.priority("P0")
def test_describe_instance_names_basic(self, setup_clients, instance_data):
"""查询用户实例的实例名基本功能测试"""
if not instance_data['native_cluster']['cacheInstanceId']:
pytest.skip("未提供缓存实例ID,跳过测试")
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
service = InstanceManagementService(setup_clients)
service.wait_for_instance_status(cacheInstanceId)
success, result = service.describe_instance_names_single(cacheInstanceId)
# 第一层:调用状态断言
assert success, f"API调用失败,实例ID: {cacheInstanceId}, 错误: {result}"
# 第二层:响应结构断言 + 第三层:字段值断言
if isinstance(result, dict):
is_valid, message = service.validate_instance_names_response(result, [cacheInstanceId])
assert is_valid, f"响应验证失败: {message},实际响应: {result}"
logging.info(f"基本功能测试通过: {message}")
else:
logging.info("API返回空结果(null)")
@pytest.mark.run(order=10)
@pytest.mark.redis_query
@pytest.mark.priority("P1")
@pytest.mark.parametrize("resource_list", ["single", "multiple"], ids=["single_instance", "multiple_instances"])
def test_describe_instance_names_parametrized(self, setup_clients, instance_data, resource_list):
"""查询用户实例的实例名参数化测试"""
if not instance_data['native_cluster']['cacheInstanceId']:
pytest.skip("未提供缓存实例ID,跳过测试")
cacheInstanceId = instance_data['native_cluster']['cacheInstanceId']
service = InstanceManagementService(setup_clients)
service.wait_for_instance_status(cacheInstanceId)
if resource_list == "single":
success, result = service.describe_instance_names_single(cacheInstanceId)
else:
success, result = service.describe_instance_names_multiple([cacheInstanceId])
# 分层断言
assert success, f"API调用失败,resource_list={resource_list},错误: {result}"
if isinstance(result, dict):
assert 'instanceNames' in result, f"响应缺少instanceNames字段,实际响应: {result}"
logging.info(f"参数化测试通过 - resource_list={resource_list}")
@pytest.mark.run(order=11)
@pytest.mark.redis_query
@pytest.mark.priority("P1")
def test_describe_instance_names_error_handling(self, setup_clients, instance_data):
"""查询用户实例的实例名错误处理测试"""
service = InstanceManagementService(setup_clients)
invalid_id = "redis-nonexistent-instance"
success, result = service.describe_instance_names_single(invalid_id)
# 断言期望失败
assert not success, f"使用无效实例ID {invalid_id} 应该失败但成功了"
logging.info(f"错误处理测试通过,错误信息: {result}")
```