# Python单元测试

## 规则（Rules）

# Python单元测试规范

## 适用对象和范围

本规范适用于所有使用pytest对Python代码进行单元测试的场景。

---

## 1. 测试框架规范

**规则**：Python单元测试必须使用pytest框架。

- ✅ 正确：`pip install pytest pytest-cov`
- ❌ 错误：使用非标准测试框架

**违反后果**：非标准框架可能导致社区支持不足，CI/CD集成困难。

---

## 2. 测试函数命名规范

**规则**：测试函数名必须以 `test_` 为前缀，测试类名必须以 `Test` 为前缀。

- ✅ 正确：`def test_get_user_data():`、`class TestUserService:`
- ❌ 错误：`def get_user_data_test():`、`class UserServiceTest:`

**违反后果**：命名不规范导致pytest无法自动发现测试用例。

---

## 3. 断言规范

**规则**：必须使用Python原生 `assert` 语句进行断言。

- ✅ 正确：`assert result['name'] == '张三'`
- ❌ 错误：使用非标准断言库

**违反后果**：非标准断言导致测试输出不统一。

---

## 4. 覆盖率规范

**规则**：行覆盖率必须达到80%以上。

- ✅ 正确：运行 `pytest --cov=src` 检查覆盖率
- ❌ 错误：覆盖率低于80%就提交代码

**违反后果**：覆盖率不足导致未测试的代码可能存在bug。

---

## 5. 测试文件命名规范

**规则**：测试文件名必须使用 `test_{模块名}.py` 格式。

- ✅ 正确：`test_user_service.py`、`test_user_controller.py`
- ❌ 错误：`user_service_test.py`、`test.py`

**违反后果**：测试文件命名不规范导致pytest无法自动发现。

---

## 6. fixture管理规范

**规则**：共享的fixture必须放在 `conftest.py` 中管理。

- ✅ 正确：`conftest.py` 中定义 `@pytest.fixture`
- ❌ 错误：在每个测试文件中重复定义相同的fixture

**违反后果**：fixture重复定义导致代码冗余，维护困难。

## 方法（Methods）

# Python单元测试方法

## 前置条件

- [ ] 已确认被测代码文件路径
- [ ] 已安装pytest和pytest-cov

## 流程概览

分析代码结构 → 编写单元测试用例 → 执行测试并获取覆盖率 → 生成测试报告 → 回归测试（代码修改后）

## 详细步骤

### 步骤1：分析代码结构
搜索Python代码文件，读取每个文件提取导出函数和类。

### 步骤2：编写单元测试用例
对每个函数编写测试用例，覆盖正常路径、边界值和异常路径。

### 步骤3：执行测试并获取覆盖率
运行pytest执行测试，使用pytest-cov获取覆盖率。

## 技巧（Tips）

# Python单元测试技巧

## 1. 使用fixture管理测试数据

**适用场景**：多个测试用例需要相同的测试数据时。

**具体做法**：使用 `@pytest.fixture` 定义可复用的测试数据。

**示例**：
```python
import pytest

@pytest.fixture
def test_user():
    return {"id": 1, "name": "张三", "email": "zhang@test.com"}

def test_get_user_name(test_user):
    assert test_user["name"] == "张三"
```

**注意事项**：fixture的作用域（scope）可以设置为 `function`、`class`、`module`、`session`。

---

## 2. 使用mock模拟外部依赖

**适用场景**：测试依赖数据库、网络请求等外部服务的函数时。

**具体做法**：使用 `unittest.mock` 或 `pytest-mock` 模拟外部依赖。

**示例**：
```python
from unittest.mock import AsyncMock, patch

async def test_get_user_data():
    mock_data = {"id": 1, "name": "张三"}
    
    with patch('services.user_service.fetch_user', new=AsyncMock(return_value=mock_data)):
        result = await get_user_data(1)
        assert result["name"] == "张三"
```

**注意事项**：`patch` 的路径要正确指向被测试模块中使用的路径。

---

## 3. 使用parametrize测试多组数据

**适用场景**：同一个测试逻辑需要测试多组输入数据时。

**具体做法**：使用 `@pytest.mark.parametrize` 装饰器。

**对比说明**：

| 维度 | 多个独立测试 | parametrize |
|------|------------|-------------|
| 代码量 | 每个数据写一个测试 | 一个测试多组数据 |
| 可读性 | 分散难对比 | 集中易对比 |
| 报告 | 多个测试条目 | 自动生成多组结果 |

**示例**：
```python
@pytest.mark.parametrize("input_data,expected", [
    ({"age": 18}, True),
    ({"age": 17}, False),
    ({"age": 0}, False),
    ({"age": -1}, False),
])
def test_is_adult(input_data, expected):
    assert is_adult(input_data) == expected
```

**注意事项**：参数组合过多时，考虑使用 `pytest.mark.parametrize` 的笛卡尔积组合。

---

## 4. 常见问题速查

| 问题 | 原因 | 解决方案 |
|------|------|---------|
| pytest找不到测试 | 文件名或函数名未以test_开头 | 确保文件名和函数名以 `test_` 开头 |
| 异步测试失败 | 未安装pytest-asyncio | 安装 `pytest-asyncio` 并添加 `@pytest.mark.asyncio` |
| mock未生效 | patch路径错误 | 检查patch的是被测试模块中实际使用的路径 |
| 覆盖率低 | 未测试分支逻辑 | 补充if/else、try/except等分支的测试 |
| fixture未定义 | 未放入conftest.py | 将共享fixture移到 `conftest.py` 中 |
