【日常学习】2025-8-27 测开框架设计模式探索04
一、ext
1. 类定义:继承 Selenium 原生的 webdriver.Remote
chromedriver是浏览器驱动程序,selenium代码发送的自动化指令通过它翻译成浏览器能识别的底层命令,浏览器执行。
反之,浏览器的执行结果也需要通过浏览器内核反馈给代码
这个原生的类的实例化就是driver驱动实例化:
1)发送请求:把调用的方法封装成符合Webdriver协议的请求发给浏览器内核
2)接受结果:反之也能反馈浏览器操作结果给代码,是浏览器内核和代码之间的桥梁
3)管理会话:从driver实例化(创建浏览器会话)到driver.quit()关闭浏览器结束会话,整个浏览器的生命周期由driver统一管理。
class WebDriverExt(webdriver.Remote):
WebDriverExt
继承了 webdriver.Remote
,说明框架:
让代码可以跨机器、跨浏览器地控制网页,能大规模自动化测试
2. __init__
方法:初始化扩展类
def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',desired_capabilities=None, browser_profile=None, proxy=None,keep_alive=True, file_detector=None):# 调用父类(webdriver.Remote)的初始化方法,保留原生功能super(WebDriverExt, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive,file_detector)self._driver = self # 给自己起个别名,方便后续调用self.initConfig() # 调用自定义的初始化配置方法
- 核心作用:创建
WebDriverExt
实例时,先按照 Selenium 原生的方式初始化(保证基础功能可用),然后执行自定义的配置。 command_executor
:默认指向本地的 Selenium Grid 服务地址(127.0.0.1:4444
),框架用了 Grid 来管理浏览器。
def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', # 远程服务器地址,默认本地Grid服务desired_capabilities=None, # 浏览器能力配置(如指定Chrome/Firefox)browser_profile=None, # 浏览器配置文件(如保存的书签、设置)proxy=None, # 代理配置keep_alive=True, # 是否保持长连接(优化性能)file_detector=None): # 文件检测器(处理文件上传)
- 均为父类
webdriver.Remote
要求的参数,这里保留默认值或设为None
,确保兼容性。
super(WebDriverExt, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive,file_detector)
super(子类名, self).__init__(参数)
:调用父类的构造方法,确保父类的初始化逻辑被执行(如连接远程服务器、初始化浏览器)。
self._driver = self # 给当前实例(self)起一个别名_driver
self._driver
:定义实例属性_driver
,值为实例本身(self
)。- 用途:后续在类内部可以通过
self._driver
调用实例的方法(和self
效果一样,可能是为了代码可读性或兼容旧逻辑)。
self.initConfig() # 调用当前类的initConfig方法,执行自定义配置
self.initConfig()
:调用本类中定义的initConfig
方法,触发额外的初始化逻辑(如设置等待时间)。
3. initConfig
方法:设置默认配置
def initConfig(self):"""初始化Driver的配置"""# 从框架配置中读取"隐式等待时间",设置给driverself.implicitly_wait(settings.DRIVER['implicitlyWait'])
implicitly_wait()
是 Selenium 的隐式等待方法(当查找元素时,如果没找到,会等待指定时间再报错)。settings.DRIVER['implicitlyWait']
:从框架的配置文件(settings
)中读取预设的等待时间(比如 10 秒),避免每次使用都手动设置,实现 “全局统一配置”。
4. windowScrollTo
方法:自定义页面滚动功能
def windowScrollTo(self, x, y):# 确保滚动坐标不小于0(避免无效值)x = 0 if x < 0 else xy = 0 if y < 0 else y# 构造JavaScript代码:滚动页面到指定坐标(x,y)js = 'window.scrollTo(%s,%s)' % (x, y)self._driver.execute_script(js) # 执行JS代码sleep(2) # 等待2秒,让滚动完成
- 这是在原生
webdriver
基础上新增的功能:封装了 “页面滚动” 操作。 - 为什么要封装?因为 Selenium 原生没有专门的滚动方法,需要通过执行 JavaScript 实现(
window.scrollTo(x,y)
是 JS 滚动页面的语法)。框架把这个操作封装成方法后,用例中可以直接调用driver.windowScrollTo(0, 500)
,不用每次写 JS 代码。 sleep(2)
:滚动后等待 2 秒,确保页面元素加载完成(避免后续操作太快导致失败)。
⭐ 作用
简单说,WebDriverExt
是 “增强版的浏览器驱动”:
- 保留了 Selenium 原生的所有功能(继承自
webdriver.Remote
); - 自动应用框架的全局配置(如隐式等待时间,通过
initConfig
实现); - 新增了常用的自定义操作(如
windowScrollTo
封装页面滚动)。
二、类
1. 类定义:继承 WebDriverExt
class WebDriver(WebDriverExt): # 定义WebDriver类,继承自WebDriverExt
2. __init__
方法:初始化并新增基础属性
def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',desired_capabilities=None, browser_profile=None, proxy=None,keep_alive=True, file_detector=None):# 调用父类(WebDriverExt)的构造方法,保留所有基础功能super(WebDriver, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive,file_detector)# 1. 给实例自身起别名(和WebDriverExt一致,可能是为了兼容旧代码)self._driver = self# 2. 新增:初始化窗口/iframe切换工具(SwitchTo)self._switch_to = SwitchTo(self)
self._switch_to = SwitchTo(self)
:这是WebDriver
相比WebDriverExt
的第一个新增核心功能!SwitchTo
是 Selenium 提供的 窗口 /iframe/ 弹窗切换工具类(比如切换到新窗口driver._switch_to.window()
、切换到 iframedriver._switch_to.frame()
)。- 这里在
WebDriver
中初始化SwitchTo
并绑定当前实例,意味着用例中创建WebDriver
对象后,能直接用driver._switch_to
做切换操作,无需手动初始化。
⭐ 分开的作用:职责分离
WebDriverExt
:负责 通用基础增强(如全局隐式等待配置、通用页面滚动),是所有项目都能复用的 “基础层”。WebDriver
:负责 专项功能扩展(如窗口切换、CDP 命令、自定义 Cookie 操作),是当前项目特有的 “业务层”。
三、SwitchTo
类
对 Selenium 原生 SwitchTo
类的 定制化扩展,主要用于更便捷、更健壮地处理 窗口切换、表单切换、弹窗(Alert)操作。它在保留原生功能的基础上,增加了对 “窗口索引切换” 的支持和自定义异常处理,让测试用例中的切换操作更符合实际使用习惯。
class SwitchTo(SeleniumSwitchTo): # 自定义SwitchTo类,继承自Selenium原生SwitchTo
def __init__(self, driver):super(SwitchTo, self).__init__(driver) # 调用父类初始化方法,保留原生初始化逻辑self._driver = driver # 保存驱动实例到当前类,后续切换操作可能需要用到driver
window
方法:窗口切换(核心扩展功能)
这是整个类最关键的增强点 —— 原生 window()
方法只能通过 “窗口句柄(字符串)” 切换,而框架扩展后支持 “整数索引” 切换,更符合测试人员的使用习惯。
#@driverAction_dec("切换到窗口", False) # 同样是预留的增强装饰器
def window(self, window_name):# 若传入的是整数(表示窗口索引,如0=第一个窗口,1=第二个窗口)if isinstance(window_name, int):try:# 1. 获取当前所有窗口的句柄(列表形式,按打开顺序排列)handles = self._driver.window_handles# 2. 获取窗口总数count = len(handles)# 3. 检查索引是否有效(0 <= 索引 < 窗口总数)if window_name < count and window_name >= 0:# 有效则切换到对应索引的窗口句柄(调用父类方法)super(SwitchTo, self).window(handles[window_name])else:# 无效则抛出自定义异常(明确提示索引越界)raise SwitchToWindowException(f'窗口索引{window_name}超出当前窗口数量最大值{count}')except Exception as e:# 其他错误(如获取句柄失败)也抛出自定义异常raise SwitchToWindowException(e)else:# 若传入的不是整数(如窗口句柄字符串),直接调用父类方法(保留原生功能)super(SwitchTo, self).window(window_name)
- 原生
window()
缺陷:必须传入窗口句柄(如CDwindow-XXX
这样的字符串),而测试人员通常更习惯用 “索引”(比如 “切换到第 2 个打开的窗口”)。 - 框架扩展后:
- 支持
window(0)
切换到第一个窗口、window(1)
切换到第二个窗口(按打开顺序); - 自动校验索引有效性,超出范围时抛出
SwitchToWindowException
(自定义异常,错误信息更明确,方便定位问题); - 兼容原生用法:如果传入窗口句柄字符串,仍能正常切换(不破坏原有功能)。
- 支持
alert
属性:弹窗处理(关联自定义 Alert
类)
@property # 装饰为属性,可通过“实例.alert”访问,无需加括号调用
def alert(self):return self.__alert() # 调用内部方法__alert()def __alert(self):return Alert(self._driver) # 返回框架自定义的Alert实例,而非原生Alert
- 作用:将原生的弹窗(Alert)处理逻辑,替换为框架自定义的
Alert
类(导入自from .alert import Alert
)。 - 为什么要自定义?原生
Alert
类功能简单(只有accept()
、dismiss()
等基础方法),框架的Alert
类可能增加了更多功能(如 “获取弹窗文本并截图”“等待弹窗出现” 等),更适配项目需求。
四、alert
# coding=utf-8 # 声明编码格式,支持中文注释(避免中文乱码)
from selenium.webdriver.common.alert import Alert as SeleniumAlert # 导入Selenium原生Alert类,起别名“SeleniumAlert”
- 关键:用
as SeleniumAlert
给原生类起别名,是为了 避免和当前自定义的Alert
类重名(如果直接from ... import Alert
,会覆盖自定义类,导致报错)。
class Alert(SeleniumAlert): # 自定义Alert类,继承自Selenium原生Alert
- 语法:
class 子类(父类)
,这是 Python 面向对象 “继承” 的基础写法,确保自定义类能复用原生类的所有方法(如accept()
、dismiss()
等)。
def __init__(self, driver):super(Alert, self).__init__(driver) # 调用父类(原生Alert)的构造函数self._driver = driver # 额外保存驱动实例到当前类
dismiss
方法:关闭弹窗(预留扩展位)
#@driverAction_dec("取消警告框",False) # 注释掉的装饰器(核心预留点)
def dismiss(self):super(Alert, self).dismiss() # 直接调用原生Alert的dismiss()方法
- 原生功能:
dismiss()
用于 “关闭弹窗”(对应弹窗的 “取消” 按钮,比如浏览器提示 “是否离开此页面” 时,点击 “取消”)。 - 框架封装意图:目前完全复用原生逻辑,没有新增代码,但注释的
@driverAction_dec
装饰器是关键 —— 这是 预留的扩展位。
比如未来想给 “关闭弹窗” 操作添加日志(记录 “何时关闭了弹窗”)或截图(关闭前截图留存证据),只需启用装饰器。
装饰器driverAction_dec
是框架自定义的工具,能自动添加日志、截图等通用增强功能,无需修改dismiss()
本身的代码。
accept
方法:确认弹窗(同 dismiss
逻辑)
#@driverAction_dec("接受警告框",False) # 同样是预留装饰器
def accept(self):super(Alert, self).accept() # 调用原生Alert的accept()方法
send_keys
方法:向弹窗输入文本(同逻辑)
#@driverAction_dec("发送文本到警告框",False) # 预留装饰器
def send_keys(self, keysToSend):super(Alert, self).send_keys(keysToSend) # 调用原生Alert的send_keys()方法
表面看,这个类只是 “重复调用原生方法”,似乎没什么用,但实际是框架设计中 “预留扩展、统一管控” 思想的体现:
- 预留扩展空间:通过注释的装饰器,未来无需修改核心代码,就能快速给弹窗操作添加日志、截图、重试等增强功能(符合 “开闭原则”:对扩展开放,对修改关闭);
- 统一 API 风格:框架的
SwitchTo
、WebDriverExt
等类都采用 “继承原生类 + 轻量封装” 的模式,Alert
类保持一致风格,方便测试人员记忆和使用(比如用driver._switch_to.alert.accept()
确认弹窗,符合框架统一逻辑); - 便于业务定制:如果未来项目有特殊弹窗需求(比如 “弹窗文本必须包含 XX 关键词才确认”),可以直接在这些方法中添加业务逻辑。
五、By
这个 WebDriverBy
类是框架中的 “元素定位方式映射转换器”,核心作用是:将框架 自定义的 By
类型(来自 frameworkCore.driver.by
),统一转换为 Selenium 原生的 By
类型(来自 selenium.webdriver.common.by
),解决 “框架自定义定位标识” 与 “Selenium 原生定位标识” 的兼容问题。
# coding:utf-8 # 支持中文注释
from frameworkCore.driver.by import By # 导入框架自定义的By类(待转换的“源类型”)
from selenium.webdriver.common.by import By as webDriverBy # 导入Selenium原生By,起别名避免重名
- 关键:用
as webDriverBy
给原生By
起别名,是为了和框架自定义的By
区分开(否则两个By
重名,会导致代码混淆)。
class WebDriverBy: # 无父类,是一个“纯静态工具类”(只存映射表和转换方法)
- 这个类不需要继承任何父类,因为它的核心功能是 “存储映射关系” 和 “提供转换方法”,本质是一个 工具类(类似 “字典 + 静态方法” 的组合,但用类封装更规整)。
类属性 byMap
:定位方式映射表
byMap = {By.ID: webDriverBy.ID, # 框架By.ID → Selenium原生ID定位By.NAME: webDriverBy.NAME, # 框架By.NAME → Selenium原生NAME定位By.CLASS_NAME: webDriverBy.CLASS_NAME, # 框架By.CLASS_NAME → 原生CLASS_NAME定位By.LINK_TEXT: webDriverBy.LINK_TEXT, # 框架By.LINK_TEXT → 原生LINK_TEXT定位By.PARTIAL_LINK_TEXT: webDriverBy.PARTIAL_LINK_TEXT, # 部分链接文本定位By.TAG_NAME: webDriverBy.TAG_NAME, # 标签名定位By.XPATH: webDriverBy.XPATH, # XPATH定位By.CSS_SELECTOR: webDriverBy.CSS_SELECTOR # CSS选择器定位
}
- 本质:这是一个 字典(key-value 映射表),作用是建立 “框架自定义
By
” 和 “Selenium 原生By
” 的一一对应关系。
类方法 convert_by
:执行定位方式转换
@classmethod # 装饰为“类方法”,无需创建实例,直接通过类调用
def convert_by(cls, by):return cls.byMap[by] # 根据传入的框架By,返回对应的原生By
WebDriverBy
就是框架和 Selenium 之间的 “翻译官”—— 让框架的自定义定位标识,能被 Selenium 看懂并执行~
六、 exceptionUtil模块
单独创建 exceptionUtil
类(或工具模块),核心目的是 “统一异常判断逻辑,实现异常处理的复用与解耦”—— 把 “判断是否为超时异常” 这类通用逻辑抽成独立工具,避免在框架各个角落重复写相同代码,同时让异常判断更规范、更易维护。
模块直接定义了函数 isTimeOutException
,本质是一个 “异常处理工具模块”(命名为 exceptionUtil.py
),而非严格意义上的 “类”。这种设计更轻量,适合封装单一、通用的异常判断逻辑。
from frameworkCore.driver.exception import TimeOutException, TimeOutException
def isTimeOutException(err):return isinstance(err, TimeOutException)
- 核心功能:判断传入的异常对象
err
,是否属于TimeOutException
类型(或其子类类型)。 - 关键语法:
isinstance(err, 异常类)
是 Python 中判断 “对象是否属于某个类(或其派生类)” 的标准方法,返回True
/False
。- 举例:如果
err
是TimeOutException("元素定位超时")
的实例,返回True
;
- 举例:如果
⭐ 为什么要单独抽成工具?(核心价值)
如果不抽这个工具,每次判断 “是否为超时异常” 都要写 isinstance(err, TimeOutException)。
假设框架中有 10 个地方需要判断超时异常(比如 Page 类的元素定位、用例中的步骤重试、日志记录等):
- 不抽工具:每个地方都要写
if isinstance(err, TimeOutException): ...
,如果未来框架要修改异常类名(比如把TimeOutException
改成ElementTimeOutException
),需要手动修改 10 处代码,极易遗漏; - 抽成工具:所有地方都调用
if exceptionUtil.isTimeOutException(err): ...
,未来修改异常类时,只需改exceptionUtil.py
中的from ... import 新异常类
和函数内的判断,1 处修改即可覆盖所有调用场景。
对比两种写法:
- 原始写法:
if isinstance(err, TimeOutException): 处理超时逻辑
- 工具写法:
if exceptionUtil.isTimeOutException(err): 处理超时逻辑
工具写法的 可读性更高—— 即使是不熟悉框架的人,看到 isTimeOutException
这个函数名,也能立刻明白 “这是在判断是否为超时异常”,无需理解 isinstance
的细节。
⭐ 实际使用场景示例
场景 1:元素定位超时重试
在框架的元素定位工具中,如果遇到超时异常,自动重试 2 次:
import exceptionUtil # 导入异常工具
from frameworkCore.driver.exception import TimeOutExceptiondef find_element_with_retry(driver, by, value, retry=2):for _ in range(retry + 1):try:return driver.find_element(by, value)except Exception as err:# 用工具判断是否为超时异常,只有超时才重试if exceptionUtil.isTimeOutException(err) and _ < retry:print(f"定位超时,第{_+1}次重试...")continue# 非超时异常,直接抛出raise err
场景 2:日志记录异常类型
在框架的日志模块中,根据异常类型输出不同级别日志:
import logging
import exceptionUtildef log_exception(err):# 判断是否为超时异常,输出WARNING级别日志if exceptionUtil.isTimeOutException(err):logging.warning(f"超时异常:{err}")# 其他异常输出ERROR级别日志else:logging.error(f"错误异常:{err}", exc_info=True)
⭐ 总结
exceptionUtil
模块(或类)的核心是 “把通用的异常判断逻辑抽成工具”,看似只封装了一行代码,却能显著提升框架的:
- 可维护性:一处修改覆盖所有调用;
- 一致性:统一异常判断标准;
- 可读性:用函数名直观表达逻辑。
这是 Python 工程化开发中 “DRY 原则(Don't Repeat Yourself,不要重复造轮子)” 的典型体现~