【接口自动化】-4- 数据代码分离 规范YAML测试用例
一、数据和代码分离
⭐ 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.load:safe_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代码很冗余,尤其是用例多、模块多的场景。 - 解决方案:
- 遍历指定目录下的所有 YAML 文件(用例文件 )。
- 对每个 YAML 文件,动态生成 Pytest 测试函数(用
pytest.mark.parametrize实现参数化 )。 - 把生成的测试函数 “绑定” 到测试类
TestAllCase,让 Pytest 能自动发现并执行。
⭐ 核心流程拆解(从 YAML 到测试用例)
-
遍历 YAML 文件:
用Path(...).glob("**/*.yaml")找到testcases目录下所有 YAML 文件(如phpwind、shopxo模块的用例 )。 -
动态生成测试函数:
对每个 YAML 文件,调用create_testcase(yaml_path),生成一个带pytest.mark.parametrize的测试函数。 -
绑定到测试类:
用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 文件,生成一个测试函数
-
等价手动写法:
手动写的话,每个 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目录及其子目录(如phpwind、shopxo),找到所有 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测试用例不符合框架的规范!")
- 参数
caseinfo:从 YAML 读取的用例数据(字典格式 )。 CaseInfo(**caseinfo):尝试用CaseInfo的字段校验字典数据:- 若 YAML 缺少
feature/story等必填字段 → 抛出异常。 - 若字段类型不匹配(如
request不是字典 )→ 抛出异常。
- 若 YAML 缺少
- 返回值:合法的用例数据会被转成
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
-
pytest.mark.parametrize:
从read_testcase(yaml_path)读取 YAML 用例数据,每条用例数据驱动一次func执行。 -
verify_yaml(caseinfo):
把 YAML 字典转成CaseInfo对象,同时校验用例格式(确保符合框架规范 )。 -
send_all_request(**new_caseinfo.request):new_caseinfo.request是用例中定义的请求信息(如method/url/params)。**是字典解包,把request字典的键值对传给send_all_request,实现接口请求发送。
⭐ 完整流程:从 YAML 到接口请求
-
编写 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 -
读取并校验用例:
read_testcase(yaml_path)读取 YAML → 得到字典caseinfo。verify_yaml(caseinfo)校验 → 转成CaseInfo对象new_caseinfo。
-
发送接口请求:
RequestUtil().send_all_request(**new_caseinfo.request)解包request字典,发送 HTTP 请求。
更多推荐

所有评论(0)