当前位置: 首页 > news >正文

【接口自动化】-7- 热加载和日志封装

一、实现用例标准化处理 

新建main_util.py文件:

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.yamlif 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 做了这些优化:

  1. 流程解耦
    把 “校验→替换→发送→提取” 的流程单独封装成函数,不再和 pytest 的用例生成逻辑混在一起,代码更清晰。
  2. 职责单一化
    create_testcase 只负责动态生成用例stand_case_flow 只负责执行用例,符合 “单一职责原则”。
  3. 可测试性提升
    stand_case_flow 可以单独调用(不需要依赖 pytest 运行 ),方便编写单元测试,确保流程逻辑正确。

二、热加载 

⭐ 核心问题:为什么需要 “热加载替换”?

之前用 Template.safe_substitute 有 2 个致命缺陷

  1. 类型丢失:如果变量是数字字符串(如 '123' ),替换后会变成整数 123,但接口可能需要字符串类型(如 '123' )。
  2. 无法二次处理:替换后的值不能直接加密、加随机数。

热加载替换 就是为了解决这些问题,让你可以在 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)

这样做的好处:

  1. 零代码扩展:测试人员不用改 Python 代码,只需在 YAML 里写函数调用,就能实现复杂逻辑(加密、随机数 )。
  2. 类型可控: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. 热加载替换完整流程
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

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 ):

import hashlibclass DebugTalk:def md5(self, text):md5 = hashlib.md5()md5.update(text.encode())return md5.hexdigest()  # 返回 md5 加密后的字符串

在 YAML 中调用:

params:sign: ${md5(access_token)}  # 调用 DebugTalk.md5(access_token)

效果
access_token 会先被读取,再通过 md5 函数加密,最终作为 sign 参数发送给接口。

5. 解决 “数字字符串类型丢失”

在 hotload_replace 中,有一段关键代码:

if isinstance(new_value, str) and new_value.isdigit():new_value = f"'{new_value}'"  # 如 `123` → `'123'`

作用
确保从 Python 函数返回的数字字符串(如 '123' ),在替换后仍然是字符串类型(保留单引号),不会被 YAML 解析成整数。


⭐ 各文件代码详细讲解(逐文件拆解)

1. debug_talk.py
import time
import yamlclass 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 hashlibmd5 = hashlib.md5()md5.update(text.encode())return md5.hexdigest()  # 返回加密后的字符串

关键设计
所有函数返回值都是字符串类型,确保 YAML 解析后类型正确;通过 getattr 动态调用,支持扩展任意函数。

2. extract_util.py
import re
import yaml
from debug_talk import DebugTalkclass 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_yamladd ),实现 “零代码扩展”。

3. YAML 用例写法
request:method: posturl: https://api.weixin.qq.com/cgi-bin/media/uploadingparams:# 调用 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 读取的真实值 )。


⭐ 总结:热加载替换的价值

  1. 解决模板替换缺陷:保留字符串类型,支持二次处理(加密、随机数 )。
  2. 零代码扩展:测试人员只需改 YAML,就能调用 Python 函数实现复杂逻辑。
  3. 流程更灵活:从 “固定变量替换” 升级为 “动态函数调用”,适配更多接口场景(如加密接口、随机数接口 )。

核心就是 “在 YAML 里写 Python 函数调用,通过动态反射执行函数,替换字符串”,实现了 “用 YAML 控制 Python 逻辑” 的黑魔法 ✨

三、热加载代码实现的详细解析 

1. fun_list = re.findall(regexp, data_str) 要是有多个函数调用 返回的列表是什摸样的?

当 data_str 中存在多个函数调用时,re.findall(regexp, data_str) 返回的 fun_list 是一个嵌套元组的列表,每个元组对应一个函数调用的信息(函数名 + 参数)。

假设 data_str 包含 3 个函数调用:

data_str = """
params:token: ${read_yaml(access_token)}sign: ${md5(token, timestamp)}random: ${get_random(100, 999)}
"""

正则表达式匹配逻辑

使用的正则 regexp = r"\$\{(.+?)\((.*?)\)\}" 会捕获:

  • 第一个分组 (.+?):匹配函数名(如 read_yamlmd5
  • 第二个分组 (.*?):匹配函数参数(如 access_tokentoken, timestamp

fun_list 的返回结果

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 是这样的列表(包含多个元组):

fun_list = [('read_yaml', 'access_token'),('md5', 'token, timestamp'),('get_random', '100, 999')
]

循环时:

for func_name, params in fun_list:print(f"函数名:{func_name},参数:{params}")

执行过程:

  1. 第一次循环:取列表第一个元组 ('read_yaml', 'access_token')

    • 自动将元组第一个元素赋值给 func_name → func_name = 'read_yaml'
    • 自动将元组第二个元素赋值给 params → params = 'access_token'
  2. 第二次循环:取列表第二个元组 ('md5', 'token, timestamp')

    • func_name = 'md5'
    • params = 'token, timestamp'
  3. 第三次循环:取列表第三个元组 ('get_random', '100, 999')

    • func_name = 'get_random'
    • params = '100, 999'

为什么这样写?

如果不用解包,代码会更繁琐:

# 不用解包的写法(更麻烦)
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' 时:

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" 时

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

# 日志配置
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. 第二步:生成日志对象(代码里调用)
import logging# 生成日志对象(关键!让代码能调用日志功能)
logger = logging.getLogger(__name__)  
  • logging.getLogger(__name__) 会根据 pytest.ini 的配置,创建一个 “符合规则的日志对象”。
  • __name__ 是 Python 的内置变量,代表当前模块名(比如 test_api.py 里,__name__ 就是 test_api),这样不同模块的日志会区分开(方便定位哪个文件产生的日志)。

3. 第三步:写入日志(业务代码里用)
# 假设这是接口请求后的响应
res = requests.get("https://example.com/api")  # 用 logger 记录 INFO 级别的日志
logger.info(res.text)  

效果
运行测试用例后,./logs/frame.log 文件会新增一行日志,格式如:

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,还有这些常用级别:

# 调试级(一般开发阶段用,生产环境关)
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 相比的优势

对比项printlogger + 封装配置
持久化保存❌ 控制台关闭就没✅ 写入文件留存
级别区分❌ 全是普通输出✅ 按 DEBUG/INFO 分类
格式统一❌ 杂乱无章✅ 带时间 / 级别 / 文件名
生产环境可用❌ 太多无用输出✅ 可控制只存 ERROR

⭐ 实际应用场景(接口自动化里怎么用)

比如在之前的 test_api.py 里,给接口请求加日志:

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 == 200except AssertionError:logger.error("登录接口断言失败!响应:%s", res.text)raise  # 抛出异常,让用例标记为失败

运行后,日志文件会清晰记录:

  • 什么时候发的请求、参数是啥;
  • 响应内容是否符合预期;
  • 断言失败时,详细错误信息会被重点标记(ERROR 级别)。

⭐ 总结

这套 “零代码极限封装” 本质是利用 Pytest 内置的日志能力,通过简单的 pytest.ini 配置 + 三行代码,就能实现:

  • 日志自动持久化到文件;
  • 级别分类清晰;
  • 格式统一易读。

特别适合接口自动化、UI 自动化等测试场景,让你在大量用例中快速定位问题,不用再靠 print 大海捞针啦~

http://www.dtcms.com/a/326054.html

相关文章:

  • .NET Core MVC中CSHTML
  • 【测试】BDD与TDD在软件测试中的对比?
  • AI蛋白质设计学习主线
  • 【智能的起源】人类如何模仿,简单的“刺激-反应”机制 智能的核心不是记忆,而是发现规律并能迁移到新场景。 最原始的智能:没有思考,只有简单条件反射
  • 首涂模板第45套主题2.0修正版苹果CMS模板奇艺主题二开源码
  • 解决 VS Code 右键菜单丢失问题
  • calamine读取xlsx文件的方法比较
  • Spring Boot 2.0 升级至 3.5 JDK 1.8 升级至 17 全面指南
  • 计算机视觉CS231n学习(7)
  • 【Altium designer】解决报错“Access violation at address...“
  • 【代码随想录day 17】 力扣 617.合并二叉树
  • python魔法方法__str__()介绍
  • 【Lua】题目小练9
  • 从零构建自定义Spring Boot Starter:打造你的专属开箱即用组件
  • 爬虫与数据分析入门:从中国大学排名爬取到数据可视化全流程
  • Go语言构建高性能AI分析网关:原理与实战
  • 设计模式笔记_结构型_组合模式
  • React(四):事件总线、setState的细节、PureComponent、ref
  • Jenkins 搭建鸿蒙打包
  • 【k8s】k8s中的几个概念性问题
  • day48 力扣739. 每日温度 力扣496.下一个更大元素 I 力扣503.下一个更大元素II
  • 轻量级解决方案:如何高效处理Word转PDF?
  • k8s的calico是什么作用,举例一下
  • 【2025最新版】PDF24 Creator,PDF编辑,合并分割,格式转换全能工具箱,本地离线版本,完全免费!
  • 【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day1
  • 【12-激活函数】
  • 【PRML】分类
  • 普通大学本科生如何入门强化学习?
  • 算法73. 矩阵置零
  • MySQL权限管理和MySQL备份