API测试用例生成模板

5 阅读14分钟

核心功能

  1. 统一测试架构:采用三层架构设计(测试用例层 → Fixture层 → Service层 → SDK层),实现职责分离和代码复用

  2. 规范测试编写流程

    • 指定文件命名规则和目录结构
    • 定义测试优先级(P0-P3)及编写顺序
    • 提供完整的测试方法模板
  3. 断言设计规范

    • 三层断言策略:调用状态 → 响应结构 → 字段值
    • 强制要求断言信息包含上下文
    • 提供多种场景的断言模板(基本功能、边界值、错误处理、列表/分页响应)
  4. 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}")
```