一、实现用例标准化处理
新建main_util.py文件:
python
def stand_case_flow(caseinfo):
# 1. 校验用例数据
case_obj = verify_yaml(caseinfo)
# 2. 替换请求中的变量(如 ${access_token})
new_request = eu.change(case_obj.request)
# 3. 发送接口请求
res = ru.send_all_request(**new_request)
# 4. 从响应中提取变量,存入 extract.yaml
if case_obj.extract:
for key, value in case_obj.extract.items():
eu.extract(res, key, *value)
这是一个标准化用例执行函数,不管你的用例是登录、支付还是文件上传,都能按这套流程执行,实现了 "一次封装,多次复用"。
1. 为什么要 "标准化流程"?
- 减少重复代码:不管多少个用例,都用同一套流程执行,不用每个用例都写一遍 "校验→替换→发送→提取"。
- 降低维护成本 :如果需要修改流程(如添加统一日志、全局断言 ),只需要改
stand_case_flow
这一个函数,所有用例自动生效。 - 提升可读性:用例执行流程清晰明了,新人接手时能快速理解 "用例是怎么跑起来的"。
2. 如何扩展这套流程?
- 添加全局日志 :在
stand_case_flow
里,发送请求前后记录日志(如print(f"发送请求:{new_request}")
)。 - 添加统一断言 :在发送请求后,检查响应状态码(如
assert res.status_code == 200
)。 - 支持更多协议 :如果需要测试 WebSocket,可以扩展
send_all_request
,让它支持 WebSocket 请求。
对比之前的 create_testcase
,新的 stand_case_flow
做了这些优化:
- 流程解耦 :
把 "校验→替换→发送→提取" 的流程单独封装成函数,不再和pytest
的用例生成逻辑混在一起,代码更清晰。 - 职责单一化 :
create_testcase
只负责动态生成用例 ,stand_case_flow
只负责执行用例,符合 "单一职责原则"。 - 可测试性提升 :
stand_case_flow
可以单独调用(不需要依赖pytest
运行 ),方便编写单元测试,确保流程逻辑正确。
二、热加载
⭐ 核心问题:为什么需要 "热加载替换"?
之前用 Template.safe_substitute
有 2 个致命缺陷:
- 类型丢失 :如果变量是数字字符串(如
'123'
),替换后会变成整数123
,但接口可能需要字符串类型(如'123'
)。 - 无法二次处理:替换后的值不能直接加密、加随机数。
热加载替换 就是为了解决这些问题,让你可以在 YAML 里直接调用 Python 函数(如 ${read_yaml(access_token)}
),实现:
- 保留字符串类型(如
'123'
不会变成123
)。 - 对变量进行二次处理(加密、生成随机数、拼接字符串 )。
⭐ 设计思想:"在 YAML 里写 Python 函数"
核心灵感来自 HttpRunner 框架 :允许在 YAML/JSON 用例中嵌入 Python 函数调用,语法是 ${函数名(参数)}
,比如:
${read_yaml(access_token)}
:调用DebugTalk.read_yaml('access_token')
。${add(1,2)}
:调用DebugTalk.add(1,2)
。
这样做的好处:
- 零代码扩展:测试人员不用改 Python 代码,只需在 YAML 里写函数调用,就能实现复杂逻辑(加密、随机数 )。
- 类型可控 :Python 函数返回值类型由自己控制(如返回字符串
'123'
而不是整数123
)。
⭐ 代码流程:热加载替换的完整链路
1. 核心文件关系
文件 | 作用 | 关键关联 |
---|---|---|
extract_util.py |
实现热加载替换逻辑 | 调用 DebugTalk 类的函数,处理 YAML 中的 ${函数()} |
debug_talk.py |
存放可在 YAML 中调用的 Python 函数 | 如 read_yaml (读 extract.yaml )、add (加法 )、get_random_number (生成随机数 ) |
extract.yaml |
存储提取的变量(如 access_token ) |
被 debug_talk.py 的 read_yaml 函数读取 |
关系总结 :
extract_util.py
解析 YAML 中的 ${函数()}
,调用 debug_talk.py
的函数处理,读取 extract.yaml
的数据,最终替换 YAML 中的占位符。
2. 热加载替换完整流程
python
def hotload_replace(self, data_str: str):
# 1. 正则匹配 YAML 中的 `${函数名(参数)}`
regexp = r"\$\{(.+?)\((.*?)\)\}" # 匹配 `${read_yaml(access_token)}`
fun_list = re.findall(regexp, data_str) # 结果:[('read_yaml', 'access_token')]
# 2. 遍历所有匹配的函数调用
for func_name, params in fun_list:
# 3. 调用 DebugTalk 类的函数
# 3.1 无参数(如 `${get_random_number()}` )
if not params:
value = getattr(DebugTalk(), func_name)()
# 3.2 有参数(如 `${read_yaml(access_token)}` )
else:
# 参数按逗号分割(如 `1,2` → [1,2] )
params_list = params.split(',')
value = getattr(DebugTalk(), func_name)(*params_list)
# 4. 处理返回值类型(确保数字字符串不会变成整数)
if isinstance(value, str) and value.isdigit():
value = f"'{value}'" # 如 `123` → `'123'`
# 5. 替换 YAML 字符串中的 `${函数()}`
old_str = f"${{{func_name}({params})}}" # 原字符串:${read_yaml(access_token)}
data_str = data_str.replace(old_str, str(value)) # 替换为函数返回值
return data_str
流程拆解:
- 正则匹配 :找到 YAML 字符串中所有
${函数(参数)}
格式的内容。 - 调用函数 :通过
getattr(DebugTalk(), 函数名)
动态调用 Python 函数。 - 处理类型 :确保数字字符串(如
'123'
)不会变成整数。 - 替换字符串 :把
${函数()}
替换为函数返回值,生成新的 YAML 字符串。
3. 如何影响接口请求
在 change
函数中,不再使用 Template
替换,而是调用 hotload_replace
:
python
def change(self, request_data: dict):
# 1. 字典转 YAML 字符串
data_str = yaml.safe_dump(request_data)
# 2. 热加载替换(替换 `${函数()}` 为 Python 函数返回值)
new_data_str = self.hotload_replace(data_str)
# 3. YAML 字符串转回字典
return yaml.safe_load(new_data_str)
效果 :
YAML 中的 ${read_yaml(access_token)}
会被替换为 debug_talk.py
中 read_yaml('access_token')
的返回值(从 extract.yaml
读取的真实值 )。
4. 处理加密、随机数
在 debug_talk.py
中添加加密函数(如 md5
):
python
import hashlib
class DebugTalk:
def md5(self, text):
md5 = hashlib.md5()
md5.update(text.encode())
return md5.hexdigest() # 返回 md5 加密后的字符串
在 YAML 中调用:
TypeScript
params:
sign: ${md5(access_token)} # 调用 DebugTalk.md5(access_token)
效果 :
access_token
会先被读取,再通过 md5
函数加密,最终作为 sign
参数发送给接口。
5. 解决 "数字字符串类型丢失"
在 hotload_replace
中,有一段关键代码:
python
if isinstance(new_value, str) and new_value.isdigit():
new_value = f"'{new_value}'" # 如 `123` → `'123'`
作用 :
确保从 Python 函数返回的数字字符串(如 '123'
),在替换后仍然是字符串类型(保留单引号),不会被 YAML 解析成整数。
⭐ 各文件代码详细讲解(逐文件拆解)
1. debug_talk.py
python
import time
import yaml
class DebugTalk:
def read_yaml(self, key):
"""读取 extract.yaml 的值"""
with open("extract.yaml", encoding="utf-8") as f:
value = yaml.safe_load(f)
return value[key] # 返回字典中的值(如 extract.yaml 的 access_token)
def add(self, a, b):
"""加法运算"""
return str(int(a) + int(b)) # 返回字符串类型(如 `'3'` 而不是 `3`)
def get_random_number(self):
"""生成随机数(时间戳)"""
return str(int(time.time())) # 返回字符串类型的时间戳
def md5(self, text):
"""MD5 加密"""
import hashlib
md5 = hashlib.md5()
md5.update(text.encode())
return md5.hexdigest() # 返回加密后的字符串
关键设计 :
所有函数返回值都是字符串类型 ,确保 YAML 解析后类型正确;通过 getattr
动态调用,支持扩展任意函数。
2. extract_util.py
python
import re
import yaml
from debug_talk import DebugTalk
class ExtractUtil:
def hotload_replace(self, data_str: str):
# 正则匹配 `${函数名(参数)}`,如 `${read_yaml(access_token)}`
regexp = r"\$\{(.+?)\((.*?)\)\}"
# 找到所有匹配的函数调用,返回列表,如 [('read_yaml', 'access_token')]
fun_list = re.findall(regexp, data_str)
for func_name, params in fun_list:
# 动态调用 DebugTalk 的函数
dt = DebugTalk()
if not params:
# 无参数,如 `${get_random_number()}`
new_value = getattr(dt, func_name)()
else:
# 有参数,如 `${add(1,2)}`,分割参数为列表
params_list = params.split(',')
new_value = getattr(dt, func_name)(*params_list)
# 处理数字字符串,确保类型是字符串(如 '123' → "'123'")
if isinstance(new_value, str) and new_value.isdigit():
new_value = f"'{new_value}'"
# 替换原字符串中的 `${函数()}` 为新值
old_str = f"${{{func_name}({params})}}"
data_str = data_str.replace(old_str, str(new_value))
return data_str
动态调用逻辑 :
通过 getattr(DebugTalk(), func_name)
,可以在运行时动态调用任意函数(如 read_yaml
、add
),实现 "零代码扩展"。
3. YAML 用例写法
TypeScript
request:
method: post
url: https://api.weixin.qq.com/cgi-bin/media/uploading
params:
# 调用 DebugTalk.read_yaml('access_token')
access_token: ${read_yaml(access_token)}
files:
media: "E:\\shu.png"
效果 :
access_token
会被替换为 debug_talk.py
中 read_yaml('access_token')
的返回值(从 extract.yaml
读取的真实值 )。
⭐ 总结:热加载替换的价值
- 解决模板替换缺陷:保留字符串类型,支持二次处理(加密、随机数 )。
- 零代码扩展:测试人员只需改 YAML,就能调用 Python 函数实现复杂逻辑。
- 流程更灵活:从 "固定变量替换" 升级为 "动态函数调用",适配更多接口场景(如加密接口、随机数接口 )。
核心就是 "在 YAML 里写 Python 函数调用,通过动态反射执行函数,替换字符串",实现了 "用 YAML 控制 Python 逻辑" 的黑魔法 ✨
三、热加载代码实现的详细解析
1. fun_list = re.findall(regexp, data_str) 要是有多个函数调用 返回的列表是什摸样的?
当 data_str
中存在多个函数调用时,re.findall(regexp, data_str)
返回的 fun_list
是一个嵌套元组的列表,每个元组对应一个函数调用的信息(函数名 + 参数)。
假设 data_str
包含 3 个函数调用:
python
data_str = """
params:
token: ${read_yaml(access_token)}
sign: ${md5(token, timestamp)}
random: ${get_random(100, 999)}
"""
正则表达式匹配逻辑
使用的正则 regexp = r"\$\{(.+?)\((.*?)\)\}"
会捕获:
- 第一个分组
(.+?)
:匹配函数名(如read_yaml
、md5
) - 第二个分组
(.*?)
:匹配函数参数(如access_token
、token, timestamp
)
fun_list
的返回结果
python
fun_list = [
('read_yaml', 'access_token'), # 第一个函数:read_yaml(access_token)
('md5', 'token, timestamp'), # 第二个函数:md5(token, timestamp)
('get_random', '100, 999') # 第三个函数:get_random(100, 999)
]
2. for func_name, params in fun_list:这个是什么语法?
for func_name, params in fun_list:
是 Python 中序列解包(Sequence Unpacking) 的语法,专门用于遍历包含元组 / 列表的可迭代对象 (如 fun_list
这种 "列表套元组" 的结构)。
通俗理解:"一次拆包两个变量"
假设 fun_list
是这样的列表(包含多个元组):
python
fun_list = [
('read_yaml', 'access_token'),
('md5', 'token, timestamp'),
('get_random', '100, 999')
]
循环时:
python
for func_name, params in fun_list:
print(f"函数名:{func_name},参数:{params}")
执行过程:
-
第一次循环:取列表第一个元组
('read_yaml', 'access_token')
- 自动将元组第一个元素赋值给
func_name
→func_name = 'read_yaml'
- 自动将元组第二个元素赋值给
params
→params = 'access_token'
- 自动将元组第一个元素赋值给
-
第二次循环:取列表第二个元组
('md5', 'token, timestamp')
func_name = 'md5'
params = 'token, timestamp'
-
第三次循环:取列表第三个元组
('get_random', '100, 999')
func_name = 'get_random'
params = '100, 999'
为什么这样写?
如果不用解包,代码会更繁琐:
python
# 不用解包的写法(更麻烦)
for item in fun_list:
func_name = item[0] # 手动取元组第一个元素
params = item[1] # 手动取元组第二个元素
print(f"函数名:{func_name},参数:{params}")
而 for func_name, params in fun_list:
直接一步完成 "取元素 + 赋值",让代码更简洁、可读性更高。
适用场景
这种语法只适用于列表中的元素是 "长度固定的元组 / 列表" 的情况:
- 如果元组有 3 个元素,就需要 3 个变量接收:
for a, b, c in list_of_tuples:
- 如果元组长度不固定,会报错(如
ValueError: too many values to unpack
)。
3. "反射"(getattr 函数的使用)是核心,它实现了 "根据字符串动态调用对象方法" 的黑魔法。
getattr(object, name)
是 Python 的内置函数,作用是 "根据字符串 name
,获取对象 object
中的属性或方法"。
举例:
当 func_name
是字符串 'get_random_number'
时:
python
func = getattr(dt, 'get_random_number') # 等价于直接写 dt.get_random_number
反射让代码 "活" 了起来 :不管 YAML 里写什么函数名,只要 DebugTalk
类里有这个方法,就能通过 getattr(dt, func_name)
动态找到并调用,不用修改 Python 代码。
4. 类型判断函数
① isinstance(new_value, str)
- 作用:判断
new_value
是否是字符串类型 (str
)。 - 场景:因为
.isdigit()
是字符串的方法,只有new_value
是字符串时,才能调用这个方法。 - 举例:
new_value = "123"
→isinstance(new_value, str)
→True
new_value = 123
(整数)→isinstance(new_value, str)
→False
② new_value.isdigit()
-
作用:判断字符串是否由纯数字组成(0-9)。
-
场景:区分 "数字字符串"(如
"123"
)和 "普通字符串"(如"abc"
、"12a"
)。 -
举例:
"123".isdigit()
→True
"abc".isdigit()
→False
"12a".isdigit()
→False
5. 字符串替换的核心逻辑,把 YAML 中 ${函数名(参数)} 格式的占位符,替换成函数执行后的真实值(new_value)
先看 old_str = f"${``{``{func_name}({params})}}"
:拼接原始占位符
这行代码用f-string 格式化字符串 ,重新拼接出 YAML 中原始的函数调用占位符(如 ${read_yaml(access_token)}
)。
语法拆解:为什么有三个 {
和三个 }
?
- f-string 中,
{}
是用于嵌入变量的标记(如f"{name}"
会替换成变量name
的值)。 - 但我们最终需要的占位符是
${函数名(参数)}
,其中包含{
和}
这两个字符,所以需要转义 :- 用
{``{
表示一个真实的{
字符 - 用
}}
表示一个真实的}
字符
- 用
举例:当 func_name = "read_yaml"
,params = "access_token"
时
python
old_str = f"${{{func_name}({params})}}"
# 拆解:
# 1. 变量替换:{func_name} → "read_yaml",{params} → "access_token"
# 2. 转义处理:${{ → "${",}} → "}"
# 最终结果:old_str = "${read_yaml(access_token)}"
再看 data_str = data_str.replace(old_str, str(new_value))
:替换占位符
这行代码调用字符串的 replace
方法,把 data_str
中所有 old_str
(原始占位符)替换成 new_value
(函数执行后的结果)。
四、日志封装
⭐ 为什么要做日志封装?
直接 print 调试信息有这些问题:
- 日志散在控制台,跑大量用例后找不到关键信息;
- 没有级别区分(DEBUG/INFO/WARNING),分不清哪些是普通信息、哪些是报错;
- 无法持久化保存,测试结束控制台清空就没了。
封装后:
- 日志按级别分类,能快速筛选关键错误;
- 自动写入文件,随时复盘历史用例执行情况;
- 格式统一(带时间、级别、文件名),排查问题更高效。
⭐ 三步实现日志封装(逐行拆解)
1. 第一步:pytest.ini
配置日志基础规则
ini
TypeScript
# 日志配置
log_file = "./logs/frame.log" # 日志文件路径(相对路径,会生成到项目的 logs 目录)
log_file_level = INFO # 文件里记录的日志级别(INFO及以上才存:INFO/WARNING/ERROR)
log_file_format = %(asctime)s %(levelname)s %(filename)s %(message)s
# 日志格式:时间+级别+文件名+具体信息
关键参数解析:
-
log_file
:指定日志保存的位置(目录要提前建好,否则报错)。 -
log_file_level
:控制 "哪些级别的日志写入文件"。比如设为INFO
,则DEBUG
级别的日志不会存到文件(适合生产环境,减少日志量)。 -
log_file_format
:定义每条日志的结构:%(asctime)s
:日志产生的时间%(levelname)s
:日志级别(DEBUG/INFO 等)%(filename)s
:产生日志的代码文件名%(message)s
:日志的具体内容(比如接口响应、自定义信息)
2. 第二步:生成日志对象(代码里调用)
python
import logging
# 生成日志对象(关键!让代码能调用日志功能)
logger = logging.getLogger(__name__)
logging.getLogger(__name__)
会根据pytest.ini
的配置,创建一个 "符合规则的日志对象"。__name__
是 Python 的内置变量,代表当前模块名(比如test_api.py
里,__name__
就是test_api
),这样不同模块的日志会区分开(方便定位哪个文件产生的日志)。
3. 第三步:写入日志(业务代码里用)
python
# 假设这是接口请求后的响应
res = requests.get("https://example.com/api")
# 用 logger 记录 INFO 级别的日志
logger.info(res.text)
效果 :
运行测试用例后,./logs/frame.log
文件会新增一行日志,格式如:
TypeScript
2024-01-01 10:00:00,123 INFO test_api.py {"code":200,"msg":"success"}
- 时间
2024-01-01 10:00:00,123
→ 知道啥时候跑的 - 级别
INFO
→ 知道是普通信息 - 文件名
test_api.py
→ 知道哪个文件触发的 - 内容
{"code":200,"msg":"success"}
→ 知道接口响应啥
⭐ 日志级别怎么用?(扩展场景)
除了 info
,还有这些常用级别:
python
# 调试级(一般开发阶段用,生产环境关)
logger.debug("这是调试信息,比如请求参数:%s", params)
# 警告级(提醒可能有问题,但不影响运行)
logger.warning("接口响应时间超过 2s,请关注!")
# 错误级(记录明确的错误,比如接口返回 500)
logger.error("接口报错:%s", res.text)
配合 log_file_level
使用 :
如果 pytest.ini
里 log_file_level = INFO
,那么:
debug
级别的日志不会写入文件 (但控制台可能显示,取决于log_level
配置);info
/warning
/error
会写入文件,方便追溯。
⭐ 和普通 print
相比的优势
对比项 | print |
logger + 封装配置 |
---|---|---|
持久化保存 | ❌ 控制台关闭就没 | ✅ 写入文件留存 |
级别区分 | ❌ 全是普通输出 | ✅ 按 DEBUG/INFO 分类 |
格式统一 | ❌ 杂乱无章 | ✅ 带时间 / 级别 / 文件名 |
生产环境可用 | ❌ 太多无用输出 | ✅ 可控制只存 ERROR |
⭐ 实际应用场景(接口自动化里怎么用)
比如在之前的 test_api.py
里,给接口请求加日志:
python
import requests
import logging
# 生成日志对象(第二步)
logger = logging.getLogger(__name__)
def test_login():
url = "http://example.com/login"
data = {"user":"test"}
# 记录 DEBUG 级别的请求参数(调试时开,生产关)
logger.debug("登录接口请求参数:%s", data)
res = requests.post(url, data=data)
# 记录 INFO 级别的响应(常规信息)
logger.info("登录接口响应:%s", res.text)
# 断言失败时,记录 ERROR 级别日志(关键错误)
try:
assert res.status_code == 200
except AssertionError:
logger.error("登录接口断言失败!响应:%s", res.text)
raise # 抛出异常,让用例标记为失败
运行后,日志文件会清晰记录:
- 什么时候发的请求、参数是啥;
- 响应内容是否符合预期;
- 断言失败时,详细错误信息会被重点标记(
ERROR
级别)。
⭐ 总结
这套 "零代码极限封装" 本质是利用 Pytest 内置的日志能力 ,通过简单的 pytest.ini
配置 + 三行代码,就能实现:
- 日志自动持久化到文件;
- 级别分类清晰;
- 格式统一易读。
特别适合接口自动化、UI 自动化等测试场景,让你在大量用例中快速定位问题,不用再靠 print
大海捞针啦~