1.接口自动化框架最小demo

依赖: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 清点一共有多少条用例,长什么样

  1. 你终端输入 pytest 回车

  2. pytest 自动读取当前目录的 pytest.ini

    ini 复制代码
    testpaths = .          # 在当前文件夹找测试
    python_files = test_*.py # 找 test_ 开头的py文件
    addopts = -s ...       # -s:打开print打印
  3. 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")
  4. 进入 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]
  5. 回到 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)
  1. 这个函数有参数 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 对象

  2. 拿到 client 实例,传给 run_case(case, client),进入 runner

  3. runner 拿到登录用例信息:method=POST,url = 登录地址,skip_auth=True(不带头 token)

  4. client.post () 发送 http 登录请求,拿到后端返回 json

  5. 执行断言:assert_code_eq 判断 code=1

  6. yaml 标记了 extract_token: true → 从返回 data 拿到 token,执行 client.set_token(token)

    👉 token存在这个 client 对象的属性里面!这个 client 是 session 夹具,全程只有这一个实例!

  7. 登录这条用例执行完成

控制台新增打印:

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

  1. 进入 test_run_case,同样需要参数 api_client ⚠️ 重点:session 级夹具,整个会话只实例化一次! pytest 不会再重新跑 conftest 里面的 api_client,直接把刚才存了 token 的同一个 client 对象拿过来复用
  2. 交给 runner.run_case
  3. 用例 skip_auth=false → client 里面有 token,请求头自动带上 Authorization
  4. client.get 发送菜品查询请求
  5. 执行 2 个断言:code=1、包含 data 字段
  6. 本条用例结束

控制台新增打印:

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 个核心知识点(这个框架最容易懵的地方)

  1. 收集阶段 ≠ 执行阶段 收集:读 yaml、清点有多少用例,不发接口 执行:才跑接口、断言

  2. fixture 懒加载 + scope="session" 不是 pytest 一启动就创建 client!什么时候第一次用到,什么时候创建;一次创建,所有用例共用 所以登录存进去的 token,下一条用例还在。

  3. 你现在靠文件名 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 在这里看起来几乎一样,只是范围更小。

相关推荐
kevinccbsg1 小时前
AI Agent 说测试都通过了?看着它跑一遍
测试
安替-AnTi1 小时前
不用 root:我用 Tailscale 打通了云手机的 ADB 通道
adb·测试·调试·云手机·claude code
狂师4 小时前
夯爆了!又一款开源企业级一站式AI智能测试平台!(附平台架构、技术选型)
人工智能·测试·全栈
DogDaoDao1 天前
Windows 双 Shell 全面对比:CMD 与 PowerShell 的技术剖析
windows·shell·测试·powershell·cmd·程序员开发
Layer1 天前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
闻缺陷则喜何志丹1 天前
计算24点
c·测试·input·output
百慕大三角1 天前
为了在 Linux CI 里跑小程序 E2E,我把微信官方基础库搬进了浏览器:wxa-runner 开源实践
微信小程序·agent·测试
vivo互联网技术1 天前
域名埋点零侵入实时校验实践:基于代理转发与规则驱动的工具设计
前端·测试·监控·代理·埋点