依赖:
pip install pytest requests pyyaml allure-pytest
目录
kotlin
mini_auto/
├── pytest.ini
├── conftest.py
├── test_api.py
├── yaml_loader.py
├── runner.py
├── client.py
├── assertions.py
└── data/
├── 01_web_login.yml
└── 02_dish_query.yml
整体总流程图(先看宏观)
ini
敲 pytest 命令
↓
pytest读取 pytest.ini 配置
↓
【收集阶段】加载test_api.py → 执行load_cases读取yaml,生成2条用例(**不发HTTP请求!**)
↓
收集全部完成,pytest知道:一共有2条测试
↓
【执行阶段】按顺序跑用例
第1条:WEB-LOGIN-001(登录)
→ 需要api_client夹具 → 第一次触发夹具,创建ApiClient
→ runner发登录POST请求
→ 断言code=1
→ 拿到token,塞进client对象里面保存
第2条:DISH-QUERY-001(菜品查询)
→ 再次使用同一个api_client(session级夹具,只创建一次!)
→ runner发GET请求,自动带上刚才存好的token
→ 断言
↓
两条跑完 → session夹具收尾
第一大部分:收集阶段(重点:不收接口、不跑测试函数 test_run_case)
收集阶段目标:pytest 清点一共有多少条用例,长什么样
-
你终端输入
pytest回车 -
pytest 自动读取当前目录的
pytest.iniinitestpaths = . # 在当前文件夹找测试 python_files = test_*.py # 找 test_ 开头的py文件 addopts = -s ... # -s:打开print打印 -
pytest 遍历当前目录,找到
test_api.py,符合test_*.py,打开、从上到下执行这个 py 文件的顶层代码bash# test_api.py 顶层代码(这一段【收集阶段就跑】!!) print("\n######## test_api.py 文件正在加载(收集阶段触发)########") all_cases = load_cases("data") # 调用 yaml_loader! print(f"######## test_api.py 加载完成,准备交给 parametrize ########\n") -
进入
yaml_loader.py的load_cases- 读取 data 文件夹,看到
01_web_login.yaml、02_dish_query.yaml - 先读 01 登录,再读 02 菜品查询
- 把每一个 yaml 字典包装成
pytest.param,带上 case_id - 返回列表
all_cases = [登录param, 查询param]
- 读取 data 文件夹,看到
-
回到 test_api.py,来到装饰器:
python@pytest.mark.parametrize("case", all_cases) def test_run_case(case, api_client): run_case(case, api_client)parametrize 收到 2 个 param → pytest 登记:这里会生成 2 条独立用例
✅ 到这里,收集阶段结束 ✅ 关键:这个阶段,
def test_run_case()函数内部代码完全没有执行 ! ✅ 这个阶段,api_client夹具也没有执行,没有创建 client 对象、没有发任何 http 请求!
控制台你会先看到这一堆(全部是收集阶段输出)
csharp
######## test_api.py 文件正在加载(收集阶段触发)########
========== 【收集阶段 START】 yaml_loader 开始读取yaml用例 ==========
[LOADER] 读到用例:WEB-LOGIN-001 - web账号登录
[LOADER] 读到用例:DISH-QUERY-001 - 查询菜品列表
========== 【收集阶段 END】 一共加载 2 条用例 ==========
######## test_api.py 加载完成,准备交给 parametrize ########
第二大部分:执行阶段(真正跑接口、跑断言,一条一条来)
pytest 收集完所有用例之后,才开始循环执行。顺序就是 param 列表顺序:先登录,后查询
👉 执行第 1 条:WEB-LOGIN-001
python
def test_run_case(case, api_client):
run_case(case, api_client)
-
这个函数有参数
api_client,pytest 去寻找这个 fixture(在 conftest.py)✅ 【夹具懒加载!】这是第一次用到 api_client,所以才执行 fixture 里面代码
python@pytest.fixture(scope="session") def api_client(): print("\n==========【夹具执行】session夹具 api_client 被调用了!==========") client = ApiClient() # new一个对象,里面自带session、token=None yield client # 后面收尾代码,现在不会跑打印:
[CLIENT] >>> 创建新的 ApiClient 对象 -
拿到 client 实例,传给
run_case(case, client),进入 runner -
runner 拿到登录用例信息:method=POST,url = 登录地址,skip_auth=True(不带头 token)
-
client.post () 发送 http 登录请求,拿到后端返回 json
-
执行断言:
assert_code_eq判断 code=1 -
yaml 标记了
extract_token: true→ 从返回 data 拿到 token,执行client.set_token(token)👉 token存在这个 client 对象的属性里面!这个 client 是 session 夹具,全程只有这一个实例!
-
登录这条用例执行完成
控制台新增打印:
csharp
==========【夹具执行】session夹具 api_client 被调用了!==========
[CLIENT] >>> 创建新的 ApiClient 对象
=====【执行阶段】开始跑用例: WEB-LOGIN-001 web账号登录 =====
[CLIENT] >>> POST请求:http://localhost:8080/admin/login,skip_auth=True
[RUNNER] >>> 接口返回结果:{"code":1,"data":{"token":"xxx"}}
[ASSERT] >>> 断言业务code,期望:1, 实际:1
[CLIENT] >>> 设置token成功,token前8位:eyJhbGci
=====【执行阶段】用例 WEB-LOGIN-001 执行完毕,断言全部通过 =====
👉 执行第 2 条:DISH-QUERY-001
- 进入
test_run_case,同样需要参数api_client⚠️ 重点:session 级夹具,整个会话只实例化一次! pytest 不会再重新跑 conftest 里面的 api_client,直接把刚才存了 token 的同一个 client 对象拿过来复用 - 交给 runner.run_case
- 用例 skip_auth=false → client 里面有 token,请求头自动带上 Authorization
- client.get 发送菜品查询请求
- 执行 2 个断言:code=1、包含 data 字段
- 本条用例结束
控制台新增打印:
ini
=====【执行阶段】开始跑用例: DISH-QUERY-001 查询菜品列表 =====
[CLIENT] >>> GET请求:http://localhost:8080/dish/list,skip_auth=False,带上token=True
[RUNNER] >>> 接口返回结果:{"code":1,"data":[...]}
[ASSERT] >>> 断言业务code,期望:1, 实际:1
[ASSERT] >>> 断言返回包含字段:data
=====【执行阶段】用例 DISH-QUERY-001 执行完毕,断言全部通过 =====
✅ 两条用例全部跑完!
pytest 会话结束,才走到 fixture yield 后面的收尾代码:
php
yield client
# 所有用例跑完之后,才执行下面这句
print("\n==========【夹具执行】整个session全部用例跑完,夹具收尾 ==========")
🎯 小白必背的 3 个核心知识点(这个框架最容易懵的地方)
-
收集阶段 ≠ 执行阶段 收集:读 yaml、清点有多少用例,不发接口 执行:才跑接口、断言
-
fixture 懒加载 + scope="session" 不是 pytest 一启动就创建 client!什么时候第一次用到,什么时候创建;一次创建,所有用例共用 所以登录存进去的 token,下一条用例还在。
-
你现在靠文件名 01、02 控制顺序,是 demo 的取巧办法,有坑
- 如果别人装了
pytest-randomly插件,会强制打乱顺序,直接崩 - 正式项目标准做法:不要用用例 A 给用例 B 传数据,每个需要登录的用例,自己在 setup 步骤内部跑登录,自给自足,互不依赖。
- 如果别人装了
📌 核心对比scope(小白重点记)
| scope | 创建时机 | 生命周期 | 能不能跨用例传递 token |
|---|---|---|---|
| session | 整个测试会话,第一次用到的时候创建 1 次 | 全部用例跑完才销毁 | ✅ 可以,共用同一个 client |
| function | 每一条用例执行前,新建一个 | 这条用例跑完就销毁 | ❌ 不行,用完就丢,隔离干净 |
一句话大白话:
- session:全班共用 1 支笔,A 写完传给 B
- function:每个人发一支全新空白的笔,写完就收走,不能传给下一个人
面试常问一句话
function scope 适合用例之间需要完全隔离、互不污染的场景;
session scope 适合想复用连接、复用登录态的场景,但代价就是用例之间有状态依赖,容易互相影响。
思考小问题,巩固一下
如果我改成 scope="module" 呢?
module = 当前这个 py 文件(test_api.py)只创建一次,这个文件里面所有用例共用同一个 client,效果和 session 在这里看起来几乎一样,只是范围更小。