零代码-Python自动化测试(pytest+request+yaml)

gittee:https://gitee.com/blackhades/no-code-auto-api-test

github:https://github.com/black1hades/auto-api-test

# 零代码接口自动化测试框架:基于 Pytest + YAML 的落地实践

> **适用人群**:测试工程师、QA、对接口自动化感兴趣的后端开发
> **读完能做什么**:30 分钟内从零搭建一套可投入日常工作的接口自动化测试体系
> **前置要求**:会用命令行、写过接口测试用例、了解 HTTP 基本概念

---

## 一、为什么要造这个轮子?

在接手新项目或维护老项目时,接口测试往往面临几个痛点:

| 痛点 | 传统做法 | 本框架的做法 |
|------|---------|------------|
| 写测试代码门槛高 | 需要会 Python/pytest | **零代码,只写 YAML** |
| 多环境切换麻烦 | 改代码 / 改环境变量 | 一行命令切换 |
| 接口间数据传递复杂 | 全局变量传来传去 | `$cache{key}` 自动注入 |
| 新人上手慢 | 读代码、学框架 | 看 YAML 示例,复制改改就能跑 |
| 报告不直观 | 看终端输出 | HTML + Allure 双报告 |

一句话概括:**测试人员只需维护 YAML 文件,框架自动生成 pytest 代码并执行,输出可视化报告。**

---

## 二、整体架构

```
┌─────────────────────────────────────────────────┐
│                    run.py (一键入口)               │
│        加载YAML → 解析 → 生成代码 → 执行 → 报告     │
└─────────────────────────────────────────────────┘
                          │
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│   data/      │  │  testcases/  │  │   report/    │
│  YAML用例     │  │  生成的测试代码 │  │  HTML/Allure │
│  (人维护)     │  │  (自动生成)    │  │  (自动输出)   │
└──────────────┘  └──────────────┘  └──────────────┘
                          │
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ config/      │  │   utils/     │  │  conftest.py │
│ 多环境配置    │  │   工具层      │  │  pytest fixture│
└──────────────┘  └──────────────┘  └──────────────┘
```

### 核心模块职责

| 模块 | 路径 | 一句话职责 |
|------|------|-----------|
| 配置中心 | `config/conf.yaml` | 管理 dev/test/staging 多套环境的 host、账号、header |
| 用例数据 | `data/*.yml` | 测试人员唯一需要关心的文件,用 YAML 描述接口调用 |
| 代码生成器 | `utils/readFilesUtils/case_generator.py` | 将 YAML 编译为 pytest 测试函数 |
| 请求引擎 | `utils/requestsUtils/request_handler.py` | 统一发送 HTTP 请求,自动合并 header、记录日志 |
| 断言引擎 | `utils/assertion/` | 支持 jsonpath 提取 + 8种比较操作符 |
| 缓存管理 | `utils/cache_process/cache_manager.py` | 实现 `$cache{key}` 语法,解决接口间数据依赖 |
| 日志系统 | `utils/logUtils/` | loguru 按天切割,控制台 INFO + 文件 DEBUG |

---

## 三、核心设计详解

### 3.1 代码生成:YAML → pytest 的编译过程

这是框架最核心的设计。理解它,你就知道"为什么只写 YAML 就能跑"。

**流程:**

```
data/login.yml           case_parser.py          case_generator.py        testcases/test_login.py
┌──────────────┐        ┌──────────────┐        ┌──────────────┐        ┌────────────────────┐
│ login_01:    │  ──►   │ 补全默认值    │  ──►   │ 拼装代码字符串  │  ──►   │ class TestLogin:   │
│   url: /auth │        │ method→GET   │        │ 写.py文件     │        │   def test_login_01│
│   assert: .. │        │ requestType  │        │              │        │     request_handler │
└──────────────┘        └──────────────┘        └──────────────┘        │     run_assertions  │
                                                                         └────────────────────┘
```

**生成的测试代码长什么样?** 以登录用例为例,框架生成的实际代码:

```python
# testcases/test_login.py (自动生成,勿手动修改)
import allure
from utils.requestsUtils.request_handler import request_handler
from utils.assertion.assert_engine import run_assertions
from utils.assertion.json_assert import extract_by_jsonpath as _extract_jsonpath

class TestLogin:
    @allure.title("登录成功-正常账号")
    def test_login_success_01(self, cache):
        """登录成功-正常账号"""
        params = {"username": "emilys", "password": "emilyspass"}
        headers = {}

        with allure.step("POST /auth/login"):
            resp = request_handler.post(
                "/auth/login",
                json_data=params,
                headers=headers,
            )

        # 提取依赖数据写入缓存
        _val = _extract_jsonpath(resp.json(), '$.accessToken')
        cache.set('login_token', _val)
        _val = _extract_jsonpath(resp.json(), '$.id')
        cache.set('user_id', _val)

        assertions = [{"jsonpath": "$.accessToken", "type": "!=", "value": None},
                       {"jsonpath": "$.username", "type": "==", "value": "emilys"}]
        assert run_assertions(resp.json(), assertions), "断言未全部通过"
```

> **关键点**:`cache` 参数由 pytest 的 session 级 fixture 自动注入,整个测试会话共享同一份缓存。

### 3.2 接口依赖:`$cache{key}` 的数据传递机制

实际业务中,接口 B 往往需要接口 A 的返回值。比如"获取用户信息"依赖"登录"返回的 token。

框架通过 **生产者-消费者** 模式解决:

```
┌─────────────────┐         ┌──────────────────┐         ┌─────────────────┐
│  login.yml      │  setup  │   CacheManager   │ $cache  │  user.yml       │
│  (生产者)        │ ──────► │   {token: "xxx"} │ ──────► │  (消费者)        │
│                 │         │   {user_id: 1}   │         │                 │
│ setup:          │         └──────────────────┘         │ headers:        │
│   - jsonpath:   │                                      │   Authorization:│
│     $.token     │                                      │   Bearer        │
│     cache_key:  │                                      │   $cache{token} │
│     login_token │                                      │                 │
└─────────────────┘                                      └─────────────────┘
```

**生产者端(login.yml)**—— 用 `setup` 提取响应字段到缓存:

```yaml
login_success_01:
  url: /auth/login
  method: POST
  requestType: json
  data:
    username: emilys
    password: emilyspass
  setup:                          # ← 关键:提取数据写入缓存
    - jsonpath: $.accessToken
      cache_key: login_token
    - jsonpath: $.id
      cache_key: user_id
```

**消费者端(user.yml)**—— 用 `$cache{key}` 读取缓存:

```yaml
get_current_user_01:
  url: /auth/me
  headers:
    Authorization: "Bearer $cache{login_token}"   # ← 自动替换为实际 token
  dependence_case: true                            # ← 标记为依赖用例
  assert:
    - jsonpath: $.id
      type: "=="
      value: "$cache{user_id}"                     # ← 断言值也能用缓存
```

**执行顺序保障**:框架按文件名字母序加载 YAML(`login.yml` → `user.yml`),所以前置用例天然先执行。

### 3.3 断言引擎:一条断言配置对应一行校验逻辑

断言配置是 YAML 中最核心的部分。每条断言由三个要素组成:

```yaml
assert:
  - jsonpath: $.products       # 1. 从响应 JSON 中提取什么字段
    type: "!="                 # 2. 用什么操作符比较
    value: null                # 3. 期望值是什么
```

**支持的 8 种操作符及使用场景:**

| 操作符 | 典型场景 | YAML 示例 |
|--------|---------|----------|
| `==` | 精确匹配 | `"type": "==", "value": 0` |
| `!=` | 不为空检查 | `"type": "!=", "value": null` |
| `>` / `>=` | 数值下限 | `"type": ">", "value": 0` |
| `<` / `<=` | 数值上限 | `"type": "<=", "value": 100` |
| `contains` | 字符串包含 | `"type": "contains", "value": "@"` |
| `in` | 枚举值校验 | `"type": "in", "value": ["a","b"]` |

**智能类型转换**:断言引擎会自动对齐 actual 和 expected 的类型。比如 YAML 里写 `value: "10"`(字符串),响应里是数字 `10`,框架会自动转换后再比较。这个细节大幅减少了因类型不一致导致的"假失败"。

### 3.4 多环境切换:一处配置,全局生效

不同环境(开发/测试/预发布)的接口地址、账号、数据库都不同。框架通过 `config/conf.yaml` 统一管理:

```yaml
active: dev    # ← 当前激活的环境

dev:
  host: https://dev-api.example.com
  account:
    username: test_user
    password: test_pass
  headers:
    Content-Type: application/json

test:
  host: https://test-api.example.com
  account:
    username: test_user
    password: test_pass
  # ...

staging:
  host: https://staging-api.example.com
  # ...
```

**切换方式有两种:**

```bash
# 方式一:改配置文件
# 编辑 config/conf.yaml,把 active: dev 改成 active: test

# 方式二:命令行参数(适合 CI/CD)
python run.py --env=test
pytest testcases/ --env=staging
```

`setting.py` 在启动时读取配置并暴露为全局变量,所有模块通过 `from config.setting import HOST` 使用,无需到处传参。

---

## 四、日常工作流(每天都要用的操作)

### 4.1 新增一个接口的测试用例

假设要测试 `GET /products/categories`(获取商品分类列表):

**Step 1**:在 `data/` 下新建或编辑一个 YAML 文件(比如 `data/products.yml` 追加):

```yaml
get_categories_01:
  name: 获取商品分类列表
  url: /products/categories
  method: GET
  assert:
    - jsonpath: "$"
      type: "!="
      value: null
```

**Step 2**:运行:

```bash
python run.py
```

**Step 3**:浏览器打开 `report/report.html` 看结果。

就这三步。不用写一行 Python 代码。

### 4.2 调试一个失败的用例

```
[FAIL] $.total | 实际值=0(int) | > | 期望值=0(int)
```

这种情况通常是断言条件写错了。去 `data/products.yml` 找到对应用例,修改 `assert` 中的 `type` 或 `value`,再跑一次。

**调试技巧**:看 `logs/当天日期.log`,里面有每个请求的完整 URL、Headers、Body 和响应,一目了然。

### 4.3 写一条依赖链路的用例(最常见)

场景:创建订单 → 查询订单,查询需要用到创建返回的订单 ID。

```yaml
# data/order.yml

# 第一步:创建订单(生产者)
create_order_01:
  name: 创建订单
  url: /orders/add
  method: POST
  requestType: json
  data:
    userId: 1
    products:
      - id: 1
        quantity: 2
  setup:                               # 写入缓存
    - jsonpath: $.id
      cache_key: order_id

  assert:
    - jsonpath: $.id
      type: "!="
      value: null

# 第二步:查询订单(消费者)
get_order_01:
  name: 查询刚创建的订单
  url: /orders/$cache{order_id}        # URL 里也能用缓存!
  method: GET
  dependence_case: true
  assert:
    - jsonpath: $.id
      type: "=="
      value: "$cache{order_id}"
```

> **注意**:同一个 YAML 文件内的用例按书写顺序执行,所以 producer 要写在 consumer 前面。

### 4.4 在 Jenkins 上跑

Jenkinsfile 已配好,支持参数化构建:

1. 打开 Jenkins → 找到项目 → **Build with Parameters**
2. 下拉选择环境:`dev` / `test` / `staging`
3. 点 **Build**
4. 构建完成后,点 **API Test Report** 查看 HTML 报告

定时触发已配置:**工作日每天早上 8 点**自动跑一次。

---

## 五、目录结构与职责速查

```
api-test/
├── config/
│   ├── conf.yaml              ← 【常改】多环境配置,切换环境/改 host/改账号
│   └── setting.py             ← 【不动】配置加载器,暴露全局变量
├── data/
│   ├── login.yml              ← 【常改】登录相关用例
│   ├── products.yml           ← 【常改】商品相关用例
│   └── user.yml               ← 【常改】用户相关用例
├── testcases/                 ← 【不动,gitignore】自动生成的测试代码
├── utils/
│   ├── assertion/             ← 【不动】断言引擎
│   ├── cache_process/          ← 【不动】缓存管理器
│   ├── logUtils/              ← 【不动】日志配置
│   ├── readFilesUtils/        ← 【不动】YAML读取 + 用例解析 + 代码生成
│   └── requestsUtils/         ← 【不动】HTTP请求封装
├── report/                    ← 【gitignore】报告输出,每次运行覆盖
├── logs/                      ← 【gitignore】日志文件,按天切割保留30天
├── conftest.py                ← 【不动】pytest fixture(环境切换、缓存注入)
├── pytest.ini                 ← 【不动】pytest 全局配置
├── run.py                     ← 【不动】一键运行入口
├── Jenkinsfile                ← 【按需改】CI 配置
└── requirements.txt           ← 【按需改】Python 依赖
```

**核心原则**:日常工作中你 **只需要动 `config/conf.yaml` 和 `data/*.yml`**,其他文件都是框架基础设施,不需要碰。

---

## 六、扩展指南(接入公司内部 API)

### 6.1 改环境配置

编辑 `config/conf.yaml`,把 `host` 改成公司 API 地址:

```yaml
dev:
  host: https://your-company-api.com    # ← 改这里
```

其他代码不需要任何改动。

### 6.2 加签名认证

如果公司 API 需要签名(如 HMAC-SHA256),只需在 `utils/requestsUtils/request_handler.py` 的 `send_request` 方法中加签名逻辑:

```python
def send_request(self, url, method="GET", headers=None, ...):
    # ... 现有代码 ...

    # 添加签名逻辑
    import hmac, hashlib, time
    timestamp = str(int(time.time()))
    sign_str = f"{method}\n{url}\n{timestamp}"
    signature = hmac.new(
        key=b"your-secret-key",
        msg=sign_str.encode(),
        digestmod=hashlib.sha256
    ).hexdigest()

    final_headers["X-Timestamp"] = timestamp
    final_headers["X-Signature"] = signature

    # ... 后续发送请求 ...
```

一处修改,所有用例生效。

### 6.3 接入数据库校验

如果需要在断言中查数据库对比,可以在 `data/` 的用例里加一个 `db_check` 字段(需扩展 `case_parser` 和 `case_generator`),或者直接在生成的测试代码中手写——不过这就违背"零代码"的理念了。更好的做法是在框架层提供内置的数据库校验函数。

---

## 七、常见问题与排错

### Q1:为什么 `python run.py` 报 `ModuleNotFoundError`?

**A**:虚拟环境没激活,或者依赖没装全。

```bash
# Windows
.venv\Scripts\activate
pip install -r requirements.txt

# Mac/Linux
source .venv/bin/activate
pip install -r requirements.txt
```

### Q2:依赖用例单独跑时报错,跑全量就正常?

**A**:这是预期行为。`$cache{key}` 的数据存在 session 级别缓存中,单独跑一个文件时前置接口没执行,缓存为空。解决:跑全量 `python run.py`,或者在 YAML 中给依赖字段设置默认值。

### Q3:断言失败但肉眼看起来没问题?

**A**:90% 的情况是类型不匹配。检查日志中 `实际值=xxx(int)` 和 `期望值=xxx(str)` 的类型标注。断言引擎已做了自动类型对齐,但 `contains` 操作符要求两边都是字符串。

### Q4:JSONPath 提取不到值?

**A**:打印响应 JSON 确认路径是否正确。常见错误:
- 少写了 `$` 前缀
- 数组没加索引:应该用 `$.data[0].name` 而不是 `$.data.name`
- 字段名大小写不对(JSON 是大小写敏感的)

### Q5:如何只跑某一个 YAML 文件的用例?

**A**:

```bash
# 先生成代码
python run.py   # 或者只做 Step 1-3,Ctrl+C 中断 pytest
# 再指定文件运行
pytest testcases/test_products.py -v
```

---

## 八、总结

这个框架的设计哲学是:

> **让测试人员专注于"测什么",而不是"怎么测"。**

| 角色 | 需要关心的 | 不需要关心的 |
|------|----------|-------------|
| 测试工程师 | YAML 用例编写、断言逻辑 | Python 代码、pytest 细节 |
| 框架维护者 | 代码生成器、断言引擎、请求封装 | 具体业务用例 |
| CI 管理员 | Jenkinsfile、定时触发 | 测试框架本身 |

**三个核心概念记住就行**:
1. **YAML 描述用例** → 框架生成代码 → pytest 执行
2. **`setup` + `$cache{key}`** = 接口间数据传递
3. **`jsonpath` + `type` + `value`** = 一条断言

---

*如果这篇文章对你有帮助,欢迎分享给团队。框架源码及 YAML 示例见项目仓库。*

评论