【日常学习】UI自动化自定义异常类
return exception # 已是自定义异常,直接返回# 把 Python 内置异常(如 ValueError、TimeoutError)转成自定义格式作用框架中可能抛出 Python 内置异常(如KeyError),此方法将其统一转为格式,保证异常处理逻辑一致。# 收集断言失败的上下文(如哪个方法的断言错了)context_info = UIAutoBaseException.get_c
UI 自动化测试框架的 “异常处理核心模块”,核心作用是:
- 定义一套 统一格式的 UI 自动化相关异常(如元素未找到、超时、不可交互);
- 自动收集异常上下文(如哪个类 / 方法抛错、当前页面标签),方便定位问题;
- 提供 异常追踪、去重处理 机制,避免重复处理同一异常;
- 让异常信息更结构化、易读(如 JSON 格式输出),降低调试成本。
一、核心基础类:UIAutoBaseException
(所有 UI 自动化异常的父类)
所有和 UI 操作相关的异常(如超时、元素未找到)都继承自它,负责统一异常的 “基础格式” 和 “上下文收集能力”。
1. __init__
方法:初始化异常核心属性
def __init__(self, message:str = '',
additional_info:dict = {},
context_info:dict = {},
exception_type="UIAutoBaseException"):
self.message = message # 核心错误信息(如“元素查找超时”)
self.exception_type = exception_type # 异常类型(如“TimeOutException”)
self.additional_info = additional_info # 额外信息(如定位器信息、浏览器版本)
self.context_info = context_info # 上下文信息(哪个类/方法抛错、文件名/行号)
self.exception_id = id(self) # 异常唯一标识(用于去重处理)
self.original_exception = None # 存储原始异常(包装其他异常时用)
super().__init__(self.message) # 调用父类 Exception 的初始化
ExceptionTracker.register_exception(self) # 自动注册到全局异常追踪器
- 关键作用:
给所有 UI 异常统一 “四要素”:错误信息、异常类型、上下文、额外信息,避免不同异常格式混乱;同时自动把异常加入全局追踪,方便后续获取。
2. wrap
方法:包装原始异常,形成异常链
def wrap(self, original_exception):
self.original_exception = original_exception # 保存原始异常
self.__cause__ = original_exception # Python 标准异常链(用 raise ... from 关联)
self.__suppress_context__ = True # 抑制冗余的上下文显示(只显示核心异常链)
return self
- 场景举例:
若 “元素查找超时” 是因为 “网络延迟导致浏览器未响应”,可通过wrap
把 “网络异常” 作为原始异常包装进来,调试时能看到完整的错误根源,而非只看到 “超时” 表面错误。
3. wrap_exception
静态方法:统一包装非自定义异常
@staticmethod
def wrap_exception(exception):
if isinstance(exception, UIAutoBaseException):
return exception # 已是自定义异常,直接返回
# 把 Python 内置异常(如 ValueError、TimeoutError)转成自定义格式
return UIAutoBaseException(
message=f"{type(exception).__name__}: {str(exception)}",
exception_type=type(exception).__name__
).wrap(exception)
- 作用:
框架中可能抛出 Python 内置异常(如KeyError
),此方法将其统一转为UIAutoBaseException
格式,保证异常处理逻辑一致。
4. getCurrentTabTitle
方法:获取当前页面标签名
def getCurrentTabTitle(self):
from frameworkCore.business.runContext import RunContext
from frameworkCore.driver.by import By
from frameworkCore.driver.locator import Locator
currentTabLoc = Locator(By.XPATH, '//div[contains(@class,"TabLabelActive")]/span', desc="获取当前窗口文本", wait=20)
try:
driver = RunContext.getRunContext().driver.get_driver() # 从上下文拿浏览器驱动
text = driver.find_element(currentTabLoc.by, currentTabLoc.path).text.replace('\t', '')
return text # 返回当前激活的标签页标题(如“业务月结”)
except Exception as e:
return ""
- 核心价值:
异常发生时,自动记录 “当前在哪个页面标签”(如 “客户管理”“报表统计”),让调试者一眼知道错误发生的页面,无需手动排查。
5. 上下文收集方法:get_caller_context
& get_caller_context_other
这两个方法是 “异常上下文收集的核心”,通过 inspect
模块解析调用堆栈,找到 “真正抛出异常的业务类 / 方法”(而非框架底层方法)。
以 get_caller_context
为例:
@staticmethod
def get_caller_context(method_name, reason=""):
result = dict()
try:
stack = inspect.stack() # 获取调用堆栈(从当前方法往上的所有调用关系)
for frame in stack[1:]: # 跳过当前方法,从上层开始遍历
funcname = frame.function # 当前帧的方法名
code_context = frame.code_context # 当前帧的代码片段
# 跳过框架底层方法(如 find_child、get_element),找到业务方法
if funcname in ["find_child", "get_element", "find_global"]:
continue
# 找到包含目标方法(如“find_element”)的代码帧
if code_context and method_name in code_context[0]:
caller_locals = frame[0].f_locals # 获取当前帧的局部变量
caller_instance = caller_locals.get('self') # 拿到“self”(业务类实例)
lineno = frame.lineno # 行号
filename = frame.filename # 文件名
if caller_instance:
# 组装上下文:业务类名、文件名/行号、方法名、错误原因
result = {
"ctrl_name": caller_instance.__class__.__name__, # 如“DailybsPage”
"filename": f"{filename}:{lineno}", # 如“D:/test.py:29”
"method_name": funcname, # 如“取消这个月的月结”
"reason": reason # 如“组件查找超时”
}
break # 找到最近的业务方法后停止
return result
except Exception:
return result
- 场景举例:
若底层find_element
方法抛错,通过遍历堆栈,能定位到是DailybsPage
类的取消这个月的月结
方法调用了find_element
,最终上下文会包含ctrl_name: DailybsPage
,让调试者直接知道是 “业务月结页面的取消方法出了问题”。
6. __str__
方法:结构化输出异常信息
def __str__(self):
msg = {
"message": self.message,
"exception_type": self.exception_type,
"context_info": self.context_info,
"additional": self.additional_info,
}
info = json.dumps(msg, ensure_ascii=False, indent=4) # 转 JSON 格式,易读
info = info.replace(r'\"', "'") # 处理转义字符
return info
- 输出效果:
异常打印时会显示结构化的 JSON,例如:{ "message": "查找元素超时", "exception_type": "TimeOutException", "context_info": { "ctrl_name": "DailybsPage", "filename": "D:/test.py:29", "method_name": "取消这个月的月结", "current_table": "业务月结" }, "additional": { "locator": "控件:日期卡片 查找方式:xpath,定位://div[@class='date-card'],超时等待:10" } }
比原生异常的 “一堆堆栈” 更直观,关键信息一目了然。
二、辅助工具类:异常追踪与协调
1. ExceptionTracker
:全局异常追踪器
class ExceptionTracker:
_last_exception = None # 记录最后一个抛出的异常
_processed_ids = set() # 记录已处理的异常 ID(避免重复处理)
@classmethod
def register_exception(cls, exception):
# 避免重复注册同一异常
exc_id = id(exception)
if exc_id not in cls._processed_ids:
cls._last_exception = exception
cls._processed_ids.add(exc_id)
@classmethod
def get_last_exception(cls):
return cls._last_exception # 测试报告/日志中可获取最新异常
@classmethod
def clear_last_exception(cls):
cls._last_exception = None
cls._processed_ids.clear() # 重置追踪器
- 作用:
全局记录异常,方便后续模块(如测试报告生成器)获取 “最新的异常信息”,无需手动传递异常对象。
2. ExceptionCoordinator
:异常协调器(防止重复处理)
class ExceptionCoordinator:
_handling_exceptions = {} # 记录正在处理的异常:{exc_id: {handlers: [], current_handler: ""}}
@classmethod
def mark_handling(cls, exc_id, handler_name):
# 标记异常开始处理(如“TimeOutException”处理器开始处理)
if exc_id not in cls._handling_exceptions:
cls._handling_exceptions[exc_id] = {
'handlers': [],
'current_handler': handler_name
}
return cls._handling_exceptions[exc_id]
@classmethod
def should_handle(cls, exc_id):
# 判断异常是否需要处理(只处理一次)
if exc_id not in cls._handling_exceptions:
return True
return not cls._handling_exceptions[exc_id].get('handled', False)
@classmethod
def mark_handled(cls, exc_id):
# 标记异常已处理(避免其他处理器重复处理)
if exc_id in cls._handling_exceptions:
cls._handling_exceptions[exc_id]['handled'] = True
- 场景举例:
框架中可能有多个异常处理器(如 “日志记录处理器”“重试处理器”),此协调器确保同一异常(如超时)只被处理一次(比如重试后成功,就不用再记录错误日志),避免资源浪费。
3. TrackedExceptionMixin
:异常追踪混入类
class TrackedExceptionMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # 调用父类初始化
ExceptionTracker.register_exception(self) # 自动注册到追踪器
- 作用:
“混入类(Mixin)” 是为了复用代码 —— 所有继承它的异常类,都不用手动写ExceptionTracker.register_exception
,初始化时会自动注册到追踪器,简化代码。
三、具体业务异常类:针对 UI 自动化的常见错误场景
这些类继承自 UIAutoBaseException
或结合 TrackedExceptionMixin
,针对具体错误场景定义,让异常类型更明确。
1. TimeOutException
:操作超时异常(最常见)
class TimeOutException(TrackedExceptionMixin, UIAutoBaseException):
"""操作超时异常(如查找元素超时)"""
def __init__(self,message='', additional_info={}):
# 收集上下文:找到调用“find_element”的业务方法
context_info = UIAutoBaseException.get_caller_context("find_element", '组件查找超时')
current_table = self.getCurrentTabTitle() # 加入当前页面标签
context_info.update({"current_table": current_table})
# 调用父类初始化,指定异常类型为“TimeOutException”
super().__init__(message=message,
context_info=context_info,
additional_info=additional_info,
exception_type="TimeOutException")
# 标记异常开始处理(避免重复)
ExceptionCoordinator.mark_handling(self.exception_id, "TimeOutException")
- 触发场景:
查找元素超过指定时间(如 10 秒)未找到,会抛出此异常,自动包含 “哪个类 / 方法超时、当前页面标签、定位器信息”。
2. ElementNotFoundException
:元素未找到异常
class ElementNotFoundException(TrackedExceptionMixin, UIAutoBaseException):
"""当UI元素未找到时抛出的异常"""
def __init__(self, element_name: str, *args):
self.message = f"元素'{element_name}'未找到" # 明确提示哪个元素未找到
super().__init__(self.message, *args)
- 场景举例:
定位 “提交按钮” 时,页面中没有该元素,抛出ElementNotFoundException("提交按钮")
,错误信息直接显示 “元素 ' 提交按钮 ' 未找到”,无需额外解析。
3. ElementNotInteractableException
:元素不可交互异常
class ElementNotInteractableException(UIAutoBaseException):
"""元素不可交互时抛出的异常(如按钮灰显)"""
def __init__(self, element_name: str, *args):
self.message = f"元素'{element_name}'不可交互"
super().__init__(self.message, *args)
- 场景举例:
页面上的 “删除按钮” 处于灰显状态(disabled),此时尝试点击会抛出此异常,明确提示 “元素 ' 删除按钮 ' 不可交互”。
4. CustomAssertionError
:自定义断言异常
class CustomAssertionError(TrackedExceptionMixin, AssertionError):
def __init__(self, desc):
# 收集断言失败的上下文(如哪个方法的断言错了)
context_info = UIAutoBaseException.get_caller_context("AssertionError", '数据比对异常')
super().__init__(desc) # 继承 Python 原生 AssertionError
self.context_info = context_info
- 场景举例:
测试 “提交后金额应为 100” 时,实际金额是 90,抛出CustomAssertionError("金额不符:预期100,实际90")
,同时包含上下文(如 “ReportPage 的 verify_amount 方法”)。
5. AvoidLoginError
:登录规避相关异常
class AvoidLoginError(Exception):
def __init__(self, err=''):
Exception.__init__(self, err)
- 作用:
简单的登录相关异常(如 “自动登录失败”“跳过登录时出错”),未继承UIAutoBaseException
,可能用于框架初始化阶段的登录逻辑,场景较独立。
⭐ 详细讲解 get_caller_context
第一步:先搭一个 “触发这个方法的场景”
在讲方法之前,先明确 谁会调用这个方法。我们模拟一个真实的 UI 自动化场景:
- 框架底层类:
ElementBase
(有find_element
方法,抛错时会调用get_caller_context
); - 业务类:
DailybsPage
(月结页面类,有取消月结
方法,会调用find_element
); - 测试用例:调用
DailybsPage.取消月结
,触发find_element
抛错,进而执行get_caller_context
。
模拟代码如下(先看场景,不用记):
# 模拟框架底层类:ElementBase(有find_element方法)
class ElementBase:
def find_element(self, locator):
# 假设查找元素失败,抛异常,调用get_caller_context
from 你的异常文件 import UIAutoBaseException
context = UIAutoBaseException.get_caller_context(method_name="find_element", reason="元素查找超时")
raise Exception(f"查找元素失败!上下文:{context}")
# 业务类:DailybsPage(月结页面,继承ElementBase)
class DailybsPage(ElementBase):
def 取消月结(self):
# 调用框架的find_element方法(这一步会抛错)
self.find_element(locator="日期卡片") # 假设这行在文件第15行
# 测试用例:触发整个流程
if __name__ == "__main__":
page = DailybsPage()
page.取消月结() # 调用业务方法,触发find_element抛错
第二步:跟踪 get_caller_context
的执行过程
当 DailybsPage.取消月结
调用 find_element
时,find_element
会执行 UIAutoBaseException.get_caller_context(method_name="find_element", reason="元素查找超时")
。
- 传入参数:
method_name="find_element"
,reason="元素查找超时"
- 目标:找到 “谁调用了
find_element
”(即DailybsPage.取消月结
)
第 1 行:初始化结果字典
result = dict()
- 干了什么:创建一个空字典,用来存最终要返回的上下文信息(比如业务类名、文件名)。
- 变量值:
result = {}
- 输出:无(只是初始化变量)
第 2 行:进入 try 块
try:
- 干了什么:捕获后续代码可能抛出的异常(比如
inspect.stack()
出错),避免方法本身崩溃。 - 变量值:无变化
- 输出:无
第 3 行:获取调用堆栈
stack = inspect.stack()
- 干了什么:用
inspect.stack()
获取当前的 “调用堆栈”(即函数调用的顺序,从当前方法往上追溯)。 - 变量值:
stack
是一个列表,每个元素是一个 “堆栈帧(frame)”,记录一个函数的信息。
结合我们的场景,stack
内容简化后如下(按调用顺序反向排列): -
[ # 帧0:当前方法 get_caller_context(自己调用自己?不,是inspect记录的当前方法) FrameInfo( function='get_caller_context', # 函数名 code_context=['stack = inspect.stack()\n'], # 当前行代码 filename='异常文件.py', # 所在文件 lineno=100, # 行号 f_locals={...} # 局部变量(暂时空) ), # 帧1:调用 get_caller_context 的方法(框架底层的 find_element) FrameInfo( function='find_element', # 函数名:框架的find_element code_context=['context = UIAutoBaseException.get_caller_context(...)\n'], # 调用代码 filename='框架类.py', # 所在文件 lineno=20, # 行号 f_locals={'self': <ElementBase实例>, 'locator': '日期卡片'} # 局部变量 ), # 帧2:调用 find_element 的方法(业务类的 取消月结) FrameInfo( function='取消月结', # 函数名:业务方法 code_context=['self.find_element(locator="日期卡片")\n'], # 调用代码 filename='业务类.py', # 所在文件 lineno=15, # 行号(我们场景中设定的行号) f_locals={'self': <DailybsPage实例>} # 局部变量:self是DailybsPage的实例 ), # 帧3:调用 取消月结 的方法(测试用例) FrameInfo( function='<module>', # 模块级代码(测试用例) code_context=['page.取消月结()\n'], # 调用代码 filename='测试用例.py', # 所在文件 lineno=30, # 行号 f_locals={'page': <DailybsPage实例>} # 局部变量 ) ]
- 输出:无(只是把堆栈信息存到
stack
变量)
第 4 行:遍历堆栈(跳过当前方法)
for frame in stack[1:]:
- 干了什么:遍历
stack
列表,但从索引1
开始(跳过索引0
的get_caller_context
自身),因为我们要找的是 “调用当前方法的上层方法”。 - 变量值:第一次循环时,
frame
是stack[1]
(即find_element
对应的帧);第二次循环是stack[2]
(即取消月结
对应的帧)。 - 输出:无(开始循环)
第 5-6 行:从帧中获取函数名和代码片段
funcname = frame.function
code_context = frame.code_context
- 干了什么:从当前遍历的 “帧” 中,提取两个关键信息:
funcname
:当前帧对应的函数名(比如find_element
或取消月结
);code_context
:当前帧中正在执行的代码片段(比如调用find_element
的那行代码)。
- 第一次循环(frame=stack [1],find_element 帧):
funcname = "find_element"
code_context = ['context = UIAutoBaseException.get_caller_context(...)\n']
- 第二次循环(frame=stack [2],取消月结帧):
funcname = "取消月结"
code_context = ['self.find_element(locator="日期卡片")\n']
- 输出:无(只是提取变量)
第 7 行:判断代码片段是否包含目标方法名
if code_context and method_name in code_context[0]:
- 干了什么:检查两个条件:
code_context
不为空(有些帧可能没有代码片段);- 代码片段中包含我们要找的
method_name
(即"find_element"
)。
- 第一次循环(find_element 帧):
code_context
不为空,且code_context[0]
是调用get_caller_context
的代码,不包含"find_element"
→ 条件不成立,跳过这个帧的后续逻辑,进入下一次循环。
- 第二次循环(取消月结帧):
code_context
不为空,且code_context[0]
是"self.find_element(locator="日期卡片")\n"
,包含"find_element"
→ 条件成立,进入后续逻辑。
- 输出:无(只是条件判断)
第 8-17 行:跳过框架底层方法(第一次循环不触发,第二次循环也不触发)
if funcname == "find_child" or funcname == "find_childs":
method_name = "find_child"
continue
if funcname == "get_element" or funcname == "get_elements":
method_name = "get_element"
continue
if funcname == "find_global" or funcname == "find_globals":
method_name = "find_global"
continue
- 干了什么:如果当前帧的函数是框架底层方法(如
find_child
、get_element
),就跳过(continue
),继续往上找业务方法。 - 第二次循环(取消月结帧):
funcname = "取消月结"
,不是框架底层方法 → 不执行这些if
,直接进入下一步。
- 输出:无
第 18 行:获取当前帧的局部变量
caller_locals = frame[0].f_locals
- 干了什么:
frame[0]
是 “帧对象”,f_locals
是该帧对应的函数的局部变量字典(比如实例方法的self
就在这里)。 - 第二次循环(取消月结帧):
caller_locals = {'self': <DailybsPage实例>}
(self
是DailybsPage
类的实例,即业务类实例)
- 输出:无(只是提取局部变量)
第 19 行:从局部变量中拿 “self”
caller_instance = caller_locals.get('self', None)
- 干了什么:从局部变量中获取
self
(实例方法的第一个参数,代表调用该方法的类实例)。如果没有self
(比如普通函数),就返回None
。 - 第二次循环(取消月结帧):
caller_instance = <DailybsPage实例>
(拿到了业务类的实例)
- 输出:无
第 20-22 行:获取代码行号、文件名、函数名
lineno = frame.lineno
filename = frame.filename
funcname = frame.function
- 干了什么:从当前帧中提取三个定位信息:
lineno
:代码行号;filename
:代码所在文件;funcname
:函数名(再次确认,避免之前的变量覆盖)。
- 第二次循环(取消月结帧):
lineno = 15
(我们场景中取消月结
方法调用find_element
的行号)filename = "业务类.py"
(业务类所在的文件)funcname = "取消月结"
(业务方法名)
- 输出:无
第 23 行:判断是否拿到了业务类实例
if caller_instance:
- 干了什么:检查是否拿到了
self
(即业务类实例)。 - 第二次循环(取消月结帧):
caller_instance = <DailybsPage实例>
→ 条件成立,进入if
内部。
- 输出:无
第 24-29 行:组装结果(核心!)
caller_class_name = caller_instance.__class__.__name__
result = {
"ctrl_name": caller_class_name,
"filename": filename + ": " + str(lineno),
"method_name": funcname,
"reason": reason,
}
- 干了什么:
caller_instance.__class__.__name__
:通过实例拿到它的类名(比如DailybsPage
);- 把 “类名、文件名 + 行号、方法名、错误原因” 组装成字典,存入
result
。
- 第二次循环(取消月结帧):
caller_class_name = "DailybsPage"
(业务类名)result = { "ctrl_name": "DailybsPage", "filename": "业务类.py: 15", "method_name": "取消月结", "reason": "元素查找超时" }
- 输出:无(只是组装结果字典)
第 30-35 行:else 分支(当前场景不触发)
else:
result = {
"ctrl_name": None,
"filename": f"{filename}:{lineno}",
"method_name": funcname,
"reason": reason,
}
- 干了什么:如果没拿到
self
(比如是普通函数),就把ctrl_name
设为None
。 - 当前场景:拿到了
self
,所以不执行。 - 输出:无
第 36 行:循环结束,返回结果
return result
- 干了什么:遍历完堆栈后,返回组装好的
result
字典。 - 变量值:
result = {"ctrl_name": "DailybsPage", "filename": "业务类.py: 15", "method_name": "取消月结", "reason": "元素查找超时"}
- 输出:这个字典会被返回给
find_element
方法,最终作为异常信息的一部分打印出来,比如 -
查找元素失败!上下文:{"ctrl_name": "DailybsPage", "filename": "业务类.py: 15", "method_name": "取消月结", "reason": "元素查找超时"}
第 37-38 行:异常捕获(当前场景不触发)
except Exception:
return result
- 干了什么:如果中间任何一步出错(比如
inspect.stack()
失败),就返回空的result
(避免方法抛错)。 - 当前场景:无异常,所以不执行。
- 输出:无
⭐ 了解多继承的混入类
一、先明确:TrackedExceptionMixin
自身的直接父类
class TrackedExceptionMixin:
"""异常追踪混入类"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # 确保调用父类的初始化
ExceptionTracker.register_exception(self) # 注册到全局追踪器
这里没有显式写 class TrackedExceptionMixin(父类):
,在 Python 3 中,所有未显式指定父类的类,默认父类都是 object
(Python 中所有类的 “顶层基类”)。
所以:TrackedExceptionMixin
自身的直接父类是 object
。
二、但更重要的是:Mixin 类的 “协作父类”(它为什么这么设计?)
TrackedExceptionMixin
是一个 Mixin 类(混入类)—— 它的核心作用不是 “单独作为父类”,而是 “混入到其他类中,复用代码”。它的 super().__init__(*args, **kwargs)
也不是为了调用 object
的 __init__
(object
的 __init__
基本没逻辑),而是为了 配合其他 “业务父类” 完成初始化。
这个 Mixin 类的实际用法是 多继承
class TimeOutException(TrackedExceptionMixin, UIAutoBaseException):
"""操作超时异常"""
def __init__(self,message='', additional_info={}):
# ... 初始化逻辑
super().__init__(message=message, ...) # 调用父类初始化
这里 TimeOutException
同时继承了两个类:
- 第一个父类:
TrackedExceptionMixin
(Mixin 类,提供 “异常注册到追踪器” 的复用逻辑); - 第二个父类:
UIAutoBaseException
(业务父类,真正的异常核心逻辑,比如异常格式、上下文收集)。
三、关键:super().__init__
到底调用哪个父类?(Python MRO 规则)
Mixin 类中的 super().__init__(*args, **kwargs)
不是调用 object
的 __init__
,而是遵循 Python 的 MRO(方法解析顺序)—— 按 “继承列表的顺序 + 深度优先” 找到下一个需要初始化的父类。
以 TimeOutException
为例,它的 MRO 顺序是:TimeOutException
→ TrackedExceptionMixin
→ UIAutoBaseException
→ Exception
→ object
所以当 TimeOutException
执行 super().__init__
时:
- 先进入
TrackedExceptionMixin
的__init__
; TrackedExceptionMixin
的super().__init__(*args, **kwargs)
会按 MRO 顺序,跳过object
,直接调用下一个父类UIAutoBaseException
的__init__
;- 这样就同时完成了两件事:
- Mixin 类的逻辑:
ExceptionTracker.register_exception(self)
(把异常注册到全局追踪器); - 业务父类的逻辑:
UIAutoBaseException
的初始化(设置异常信息、上下文等)。
- Mixin 类的逻辑:
总结:Mixin 类的父类逻辑
- 自身直接父类:
object
(Python 3 默认),但这几乎不重要; - 实际协作父类:它被多继承时,紧跟在它后面的 “业务类”(如
UIAutoBaseException
),这才是 Mixin 类的核心 —— 通过super()
配合业务类完成初始化,同时复用自己的代码(如异常注册)。
Mixin 类的设计精髓就是:不破坏原有类的继承关系,通过多继承 “插入” 复用逻辑,让代码更灵活(比如其他异常类如 ElementNotFoundException
也能继承这个 Mixin,快速获得 “异常追踪” 能力)。
⭐ TrackedException
类
是一个增强型异常类,继承自 Python 内置的 Exception
,主要作用是在普通异常的基础上,添加上下文追踪、原始异常包装、全局注册等功能,方便在复杂系统(比如 UI 自动化框架)中更精准地定位和管理异常。
1. 方法定义与参数说明
def __init__(self, message, context_info=None, original_exception=None):
- 参数作用:
message
:异常的核心错误信息(必填,比如 “元素点击失败”);context_info
:异常发生的上下文信息(可选,比如哪个类 / 方法出错、代码位置等);original_exception
:被包装的原始异常(可选,比如将底层的TimeoutException
包装成TrackedException
时使用)。
2. 确保上下文信息(context_info
)规范化
# 确保 context_info 包含规范化的字段
if not context_info:
context_info = UIAutoBaseException.get_caller_context_other("general_error", "未知异常")
- 作用:保证
context_info
始终有值,且格式统一(方便后续日志输出或追踪)。 - 细节:
- 如果外部调用时没传
context_info
(比如raise TrackedException("错误")
),就通过UIAutoBaseException.get_caller_context_other
生成一个默认上下文; - 默认上下文类型是
"general_error"
(通用错误),原因是"未知异常"
,同时包含调用堆栈信息(类名、方法名、代码位置等,由get_caller_context_other
实现)。
- 如果外部调用时没传
3. 调用父类(Exception
)的初始化
super().__init__(message)
- 作用:继承 Python 内置
Exception
的核心功能,确保TrackedException
是一个 “标准异常”(比如可以被try-except
捕获,包含message
属性等)。 - 为什么要调用:如果不调用
super()
,Exception
的初始化逻辑(比如message
的存储)不会执行,可能导致str(exception)
等基础功能异常。
4. 初始化核心属性(保存上下文与原始异常)
self.context_info = context_info # 保存上下文信息
self.original_exception = original_exception # 保存原始异常(如果有)
self.handling_markers = {} # 用于标记异常的处理状态(如是否被重试、忽略等)
- 属性用途:
context_info
:存储异常发生的环境信息(比如{"ctrl_name": "LoginPage", "method_name": "输入密码", "filename": "login.py:25"}
),方便定位问题;original_exception
:当需要 “包装” 底层异常时使用(比如把 Selenium 的NoSuchElementException
包装成TrackedException
),保留原始异常的全部信息;handling_markers
:一个空字典,用于后续标记异常的处理状态(比如{"retried": True, "ignored": False}
表示 “已重试但未忽略”)。
5. 处理原始异常(original_exception
):保留关键信息
# 保留原始堆栈
if original_exception:
# 获取原始异常的 exception_id(如果有),否则用原始异常的内存地址作为ID
self.exception_id = original_exception.exception_id if hasattr(original_exception, 'exception_id') else id(original_exception)
# 获取原始异常的类型(如果有),否则用原始异常的类名
self.exception_type = getattr(original_exception, 'exception_type', type(original_exception).__name__)
# 保留原始异常的堆栈跟踪(__traceback__ 是 Python 存储堆栈的属性)
self.__traceback__ = original_exception.__traceback__
- 核心目的:当
TrackedException
用于包装其他异常时(比如将底层框架异常转换为业务异常),完整保留原始异常的关键信息,避免调试时丢失上下文。 - 细节说明:
exception_id
:异常的唯一标识(用于全局追踪,避免重复处理),优先用原始异常自带的exception_id
(如果是TrackedException
或其子类),否则用id(original_exception)
(内存地址,确保唯一);exception_type
:记录原始异常的类型(比如NoSuchElementException
),方便区分异常来源;__traceback__
:保留原始异常的堆栈跟踪(即错误发生的代码调用链),这样用traceback
模块打印时,能看到最底层的错误位置。
6. 无原始异常时:为当前异常创建标识
else:
# 为自己创建属性
self.exception_id = id(self) # 用当前异常实例的内存地址作为唯一ID
self.exception_type = "TrackedException" # 明确当前异常的类型
- 作用:如果没有包装原始异常(即直接抛出
TrackedException
),则为当前异常生成自己的exception_id
和exception_type
,确保标识完整。
7. 注册到全局追踪器
# 注册到全局追踪器
ExceptionTracker.register_exception(self)
- 作用:将当前异常实例注册到全局的
ExceptionTracker
中,方便后续在系统任何地方获取 “最近发生的异常” 或 “所有异常记录”。 - 实际用途:比如在测试报告生成时,可以通过
ExceptionTracker.get_last_exception()
获取最后一个异常,自动填充错误详情;或者在异常监控中,统计异常发生的频率。
总结:TrackedException
的核心价值
这个类通过以下设计,解决了普通异常的痛点:
- 上下文追踪:强制保留异常发生的环境信息(类、方法、代码位置),避免 “只知道报错,不知道在哪报错”;
- 异常包装:可以安全包装底层异常,保留原始堆栈和标识,方便溯源;
- 全局管理:通过注册到
ExceptionTracker
,实现异常的集中管理和查询,适合复杂系统的调试和监控。
简单说:TrackedException
是一个 “带黑匣子的异常”,既能像普通异常一样被捕获,又能记录更多调试所需的 “现场信息”,还支持全局追踪。
更多推荐
所有评论(0)