一、数据和代码分离 

⭐ test_login.yaml(用例数据文件)

  • 作用:用 YAML 描述 登录接口的测试用例,包括请求参数、预期结果,实现 “用例数据和代码分离”。
  • feature: 论坛模块        # 功能模块描述
    story: 登录接口          # 测试场景
    title: 验证phpwind登录接口成功返回  # 用例标题
    request:                # 请求详情
      method: post          # 请求方法(POST)
      url: http://47.107.116.139/phpwind/index.php?m=u&c=login&a=dorun  # 接口地址
      data:                 # 请求体(表单参数)
        username: baili     # 用户名
        password: baili123  # 密码
        csrf_token: ""      # CSRF Token(需从其他接口关联获取,目前为空)
        backurl: http://47.107.116.139/phpwind/  # 登录后跳转地址
        invite: ""          # 邀请码(选填)
      headers:              # 请求头
        Accept: "application/json, text/javascript, */*; q=0.01"
        X-Requested-With: XMLHttpRequest
    validate: null          # 断言(当前为空,可扩展校验响应)
    

⭐ test_api.py(测试用例代码)

  • 作用:读取 YAML 用例数据,发送 HTTP 请求,实现 “接口关联(如 CSRF Token 传递)”,并执行测试。
  • import pytest
    import re
    from commons.yaml_util import read_yaml, write_yaml  # 假设工具类封装了 YAML 读写
    from commons.request_util import RequestUtil         # 假设工具类封装了请求发送
    
    # 读取 YAML 用例数据(pytest 参数化)
    @pytest.mark.parametrize("caseinfo", read_testcase("./testcases/phpwind/test_login.yaml"))
    def test_login(self, caseinfo):
        # 1. 解析用例数据
        method = caseinfo["request"]["method"]  # 从 YAML 取请求方法(post)
        url = caseinfo["request"]["url"]        # 从 YAML 取接口地址
        data = caseinfo["request"]["data"]      # 从 YAML 取请求体(含 username、password 等)
        headers = caseinfo["request"]["headers"]# 从 YAML 取请求头
    
        # 2. 解决 CSRF Token 关联(关键!)
        # 假设在其他接口(如 test_phpwind)中,已通过正则提取 csrf_token 并写入 YAML
        csrf_token = read_yaml("csrf_token")    # 从 YAML 读取预先保存的 CSRF Token
        data["csrf_token"] = csrf_token         # 替换 YAML 中 csrf_token 的空值
    
        # 3. 发送登录请求
        res = RequestUtil().send_all_request(
            method=method, 
            url=url, 
            data=data, 
            headers=headers
        )
    
        # 4. 调试输出(查看响应)
        print(res.json())
    
    # (补充:通常还有一个接口用来获取 CSRF Token,比如论坛首页)
    def test_phpwind(self):
        url = "http://47.107.116.139/phpwind/"
        res = RequestUtil().send_all_request(method="get", url=url)
        # 正则提取 CSRF Token(论坛页面源码中)
        search_value = re.search('name="csrf_token" value="(.*?)"', res.text)
        csrf_token = search_value.group(1) if search_value else ""
        write_yaml({"csrf_token": csrf_token})  # 写入 YAML,供登录接口使用
        assert "本站新帖" in res.text  # 简单断言首页加载正常
    

⭐ yaml_util.py(处理文件的 common包下的工具文件,之前处理token的函数也在这里)

def read_testcase(yaml_path):
    # 1. 打开 YAML 文件
    with open(yaml_path, encoding="utf-8") as f:  
        # 2. 用 yaml.safe_load 解析文件内容
        value = yaml.safe_load(f)  
        # 3. 返回解析后的数据(字典/列表)
        return value  
1. with open(yaml_path, encoding="utf-8") as f
  • 作用:以只读模式打开 YAML 文件,encoding="utf-8" 确保中文路径 / 内容不会乱码。
  • 细节:with 语法会自动关闭文件,避免资源泄漏。
2. yaml.safe_load(f)
  • 作用:把 YAML 文件内容解析成 Python 原生数据类型(字典、列表、字符串等 )。
  • 对比 yaml.loadsafe_load 更安全,避免 YAML 文件中的恶意代码执行,生产环境推荐用它。
3. return value
  • 作用:把解析后的 YAML 数据返回给调用者(比如 pytest 用例 ),让测试代码能直接使用。


⭐ 核心逻辑:“CSRF Token 关联” 的完整流程

  • 步骤 1:获取 CSRF Token(论坛首页接口)

    • 访问论坛首页(test_phpwind 接口),从响应 HTML 中用正则提取 csrf_token
    • 通过 write_yaml 将 csrf_token 写入 YAML 文件,保存为中间变量。
  • 步骤 2:登录接口使用 CSRF Token

    • 登录接口(test_login)执行时,从 YAML 读取 csrf_token,替换请求体中 csrf_token: "" 的空值。
    • 带着完整参数(含 csrf_token)发送登录请求,完成接口关联。

再举一对例子:

yaml

feature: 公众号模块                # 功能模块分类
story: 获取鉴权码接口              # 测试场景
title: 验证获取鉴权码接口成功返回   # 用例标题
request:
  method: get                      # 请求方法(GET)
  url: https://api.weixin.qq.com/cgi-bin/token  # 微信官方接口地址
  params:                          # 请求参数
    grant_type: client_credential  # 固定值,代表“客户端凭证”方式
    appid: wx8a9de038e93f77ab      # 你的公众号 AppID(需替换为真实值)
    secret: 8326fc915928dee3165720c910effb86  # 你的公众号 Secret(需替换为真实值)
validate: null                     # 断言(暂时为空,可扩展校验响应)

 test_api.py

# 假设已导入依赖:
# pytest(测试框架)、jsonpath(解析 JSON 响应)、RequestUtil(封装请求)、write_yaml(写入 YAML)
@pytest.mark.parametrize("caseinfo", read_testcase("./testcases/xxx/test_upload.yaml"))
def test_get_token(self, caseinfo):
    # 1. 解析用例数据
    method = caseinfo["request"]["method"]  # 取请求方法(get)
    url = caseinfo["request"]["url"]        # 取接口地址(微信 token 接口)
    params = caseinfo["request"]["params"]  # 取请求参数(appid、secret 等)

    # 2. 发送请求,调用微信接口
    res = RequestUtil().send_all_request(
        method=method, 
        url=url, 
        params=params  # GET 请求的参数通过 params 传递
    )

    # 3. 解析响应,提取 access_token
    # res.json() 是接口返回的 JSON,结构类似:{"access_token": "xxx", "expires_in": 7200}
    jsonpath_value = jsonpath.jsonpath(res.json(), '$.access_token')  
    access_token = jsonpath_value[0]  # 提取 access_token 的值

    # 4. 存入 yaml,作为中间变量
    data = {"access_token": access_token}  
    write_yaml(data)  # 写入 extract.yaml,供其他接口(如上传文件)使用

    # 5. 调试输出
    print(data)  # 打印存入 yaml 的数据

test_file_upload 完整流程

yaml

feature: 公众号模块                # 功能模块分类
story: 获取鉴权码接口              # 测试场景(命名可能需要优化,实际是“文件上传”场景 )
title: 验证获取鉴权码接口成功返回   # 用例标题(同样建议优化为“验证文件上传接口” )
request:
  method: post                     # 请求方法(POST)
  url: https://api.weixin.qq.com/cgi-bin/media/upload  # 微信文件上传接口
  params:                          # 请求参数
    access_token: ""               # 需动态填入(从 yaml 读取)
  data:                            # 上传文件信息
    media: "E:\\shu.png"           # 本地文件路径(需替换为真实文件)
validate: null                     # 断言(暂时为空,可扩展校验响应)

test_api.py

# 假设已导入依赖:
# pytest(测试框架)、jsonpath(解析 JSON 响应)、RequestUtil(封装请求)、read_yaml/write_yaml(读写 yaml)
@pytest.mark.parametrize("caseinfo", read_testcase("./testcases/phpwind/test_file_upload.yaml"))
def test_file_upload(self, caseinfo):
    # 1. 解析用例数据
    method = caseinfo["request"]["method"]  # 取请求方法(post)
    url = caseinfo["request"]["url"]        # 取接口地址(微信文件上传接口)
    params = caseinfo["request"]["params"]  # 取请求参数(含 access_token: "")
    data = caseinfo["request"]["data"]      # 取上传文件信息(media 路径)

    # 2. 关键!从 yaml 读取 access_token,填充到请求参数
    params["access_token"] = read_yaml("access_token")  

    # 3. 发送文件上传请求
    res = RequestUtil().send_all_request(
        method=method, 
        url=url, 
        params=params,  # GET/POST 参数通过 params 传递
        files=data      # 上传文件需用 files 传递(区别于普通 POST)
    )

    # 4. 调试输出
    print(res.json())  # 打印响应,确认是否上传成功

结果就发现,他们的共同点都是——在yaml文件取请求四要素,集齐参数发送请求,处理接口关联,其他处理。

二、零代码封装 

这段代码实现了一个 “自动化生成 Pytest 测试用例” 的框架,核心是 “根据 YAML 文件自动创建测试函数”,让你无需手动写每个用例代码,只需维护 YAML 文件即可。


⭐ 整体目标:“YAML 文件 → 自动生成测试用例”

  • 痛点:手动写大量 pytest.mark.parametrize 代码很冗余,尤其是用例多、模块多的场景。
  • 解决方案
    1. 遍历指定目录下的所有 YAML 文件(用例文件 )。
    2. 对每个 YAML 文件,动态生成 Pytest 测试函数(用 pytest.mark.parametrize 实现参数化 )。
    3. 把生成的测试函数 “绑定” 到测试类 TestAllCase,让 Pytest 能自动发现并执行。

⭐ 核心流程拆解(从 YAML 到测试用例)

  1. 遍历 YAML 文件
    用 Path(...).glob("**/*.yaml") 找到 testcases 目录下所有 YAML 文件(如 phpwindshopxo 模块的用例 )。

  2. 动态生成测试函数
    对每个 YAML 文件,调用 create_testcase(yaml_path),生成一个带 pytest.mark.parametrize 的测试函数。

  3. 绑定到测试类
    用 setattr(TestAllCase, ...) 把生成的测试函数添加到 TestAllCase 类,Pytest 执行时会自动识别。

⭐ 代码逐行解析(重点看 test_all_case.py 和 run.py

1. test_all_case.py(核心逻辑:动态生成测试用例)
from commons.yaml_util import read_testcase  # 假设封装了 YAML 读取
import pytest
from pathlib import Path  # 处理路径

# 空测试类,用于“绑定”动态生成的测试函数
class TestAllCase:
    pass

# 动态生成测试用例的工厂函数
def create_testcase(yaml_path):
    # 定义一个内部函数 func,作为测试用例的载体
    @pytest.mark.parametrize("caseinfo", read_testcase(yaml_path))
    def func(self, caseinfo):
        # 这里可以扩展:发送请求、断言等逻辑(当前仅打印用例数据)
        print(caseinfo)
    # 返回生成的测试函数
    return func

# 遍历 YAML 文件,动态生成测试用例
# 1. 获取 testcases 目录路径
testcase_path = Path(__file__).parent  # __file__ 是当前脚本路径,parent 是上级目录(testcases)
# 2. 找到所有 YAML 文件(递归遍历子目录)
yaml_case_list = testcase_path.glob("**/*.yaml")

# 3. 对每个 YAML 文件,生成测试函数并绑定到 TestAllCase
for yaml_path in yaml_case_list:
    # a. 生成测试函数(带 parametrize 的 func)
    test_func = create_testcase(yaml_path)
    # b. 给测试函数起名(如 test_xxx,xxx 是 YAML 文件名 )
    func_name = f"test_{yaml_path.stem}"  # yaml_path.stem 是文件名(不含后缀)
    # c. 绑定到 TestAllCase 类
    setattr(TestAllCase, func_name, test_func)
2. run.py(执行入口)
import pytest

if __name__ == '__main__':
    # 执行 Pytest,会自动发现 TestAllCase 类及动态生成的测试函数
    pytest.main()
    # (可选)生成 Allure 报告(需先安装 Allure 并配置 )
    # os.system("allure generate ./temps -o ./reports --clean")
    # os.system("allure open ./reports")

⭐ 关键逻辑详解(动态生成的核心)

1. create_testcase(yaml_path):生成测试函数
def create_testcase(yaml_path):
    @pytest.mark.parametrize("caseinfo", read_testcase(yaml_path))
    def func(self, caseinfo):
        print(caseinfo)
    return func

  • 作用

    • 对每个 YAML 文件,生成一个测试函数 func,并用 pytest.mark.parametrize 实现参数化(每个 YAML 用例数据对应一条测试用例 )。
    • read_testcase(yaml_path) 读取 YAML 文件内容,作为参数化的数据源。
  • 等价手动写法
    手动写的话,每个 YAML 文件需要这样写:

    @pytest.mark.parametrize("caseinfo", read_testcase("testcases/shopxo/login.yaml"))
    def test_login(self, caseinfo):
        print(caseinfo)
    
     

    而 create_testcase 帮你自动生成这段代码,无需手动写。

2. setattr(TestAllCase, ...):绑定测试函数到类
func_name = f"test_{yaml_path.stem}"  # 生成函数名(如 test_login)
setattr(TestAllCase, func_name, test_func)

  • 作用
    把动态生成的测试函数(如 test_login )添加到 TestAllCase 类,让 Pytest 执行时能自动发现这些函数。

  • Pytest 规则
    Pytest 会自动收集以 test_ 开头的函数测试类中的测试方法,这里通过 setattr 动态添加,绕过了 “手动写测试函数” 的过程。

3. 遍历 YAML 文件:testcase_path.glob("**/*.yaml")

  • 作用:递归遍历 testcases 目录及其子目录(如 phpwindshopxo ),找到所有 YAML 文件。
  • 示例
    假设目录结构:

    plaintext

    testcases/
      ├── phpwind/
      │   └── login.yaml
      └── shopxo/
          └── order.yaml
    

    则 yaml_case_list 会包含 login.yaml 和 order.yaml,分别生成 test_login 和 test_order 函数。

执行顺序:根据ini的配置,先去项目里面找testcases文件夹,然后找test_开头的py文件,从24行往下执行,到了反射函数执行17行创建用例函数以及加到TestAllCase类里面,创建所有的用例函数循环完成后去执行第13行的类。

三、Yaml测试用例的规范 

⭐ dataclass 定义用例规范(CaseInfo 类)

from dataclasses import dataclass

@dataclass
class CaseInfo:
    # 必填字段(用例必须包含这些信息)
    feature: str  # 功能模块(如“公众号模块”)
    story: str    # 测试场景(如“获取鉴权码接口”)
    title: str    # 用例标题(如“验证获取鉴权码接口成功返回”)
    request: dict # 请求信息(method、url、params 等)
    validate: dict# 断言信息(如状态码、响应内容校验)
    
    # 选填字段(可扩展,当前示例未体现)
    # 选填
    # ...

核心价值
用 @dataclass 定义 “测试用例的数据模型”,强制约束 YAML 用例的结构(必须包含 feature/story/title 等字段 ),让用例格式标准化。

⭐ verify_yaml 校验用例合法性(model_util.py

# 校验测试用例是否符合 CaseInfo 规范
def verify_yaml(caseinfo: dict):
    try:
        # 关键:用 CaseInfo 类“解析”字典数据
        new_caseinfo = CaseInfo(**caseinfo)  
        return new_caseinfo
    except Exception:
        # 若解析失败,抛出明确异常
        raise Exception("YAML测试用例不符合框架的规范!")

  1. 参数 caseinfo:从 YAML 读取的用例数据(字典格式 )。
  2. CaseInfo(**caseinfo):尝试用 CaseInfo 的字段校验字典数据
    • 若 YAML 缺少 feature/story 等必填字段 → 抛出异常。
    • 若字段类型不匹配(如 request 不是字典 )→ 抛出异常。
  3. 返回值:合法的用例数据会被转成 CaseInfo 对象,方便后续代码通过属性(如 new_caseinfo.request )访问。

⭐ create_testcase 生成用例并执行(test_all_case.py

def create_testcase(yaml_path):
    # 参数化装饰器:用 YAML 用例数据驱动测试
    @pytest.mark.parametrize("caseinfo", read_testcase(yaml_path))
    def func(self, caseinfo):
        # 1. 校验用例合法性
        new_caseinfo = verify_yaml(caseinfo)  
        
        # 2. 发送接口请求
        RequestUtil().send_all_request(**new_caseinfo.request)  
    return func

  1. pytest.mark.parametrize
    从 read_testcase(yaml_path) 读取 YAML 用例数据,每条用例数据驱动一次 func 执行

  2. verify_yaml(caseinfo)
    把 YAML 字典转成 CaseInfo 对象,同时校验用例格式(确保符合框架规范 )。

  3. send_all_request(**new_caseinfo.request)

    • new_caseinfo.request 是用例中定义的请求信息(如 method/url/params )。
    • ** 是字典解包,把 request 字典的键值对传给 send_all_request,实现接口请求发送。

⭐ 完整流程:从 YAML 到接口请求

  1. 编写 YAML 用例
    按 CaseInfo 规范写用例(如公众号文件上传接口 ):

    yaml

    feature: 公众号模块
    story: 获取鉴权码接口
    title: 验证获取鉴权码接口成功返回
    request:
      method: post
      url: https://api.weixin.qq.com/cgi-bin/media/uploadimg
      params:
        access_token: ""
      data:
        media: "E:\\shu.png"
    validate: null
    
  2. 读取并校验用例

    • read_testcase(yaml_path) 读取 YAML → 得到字典 caseinfo
    • verify_yaml(caseinfo) 校验 → 转成 CaseInfo 对象 new_caseinfo
  3. 发送接口请求
    RequestUtil().send_all_request(**new_caseinfo.request) 解包 request 字典,发送 HTTP 请求。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐