零代码-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 示例见项目仓库。*
评论