【日常学习】2025-8-27 测开框架设计模式探索04

一、ext

1. 类定义:继承 Selenium 原生的 webdriver.Remote

chromedriver是浏览器驱动程序,selenium代码发送的自动化指令通过它翻译成浏览器能识别的底层命令,浏览器执行。

反之,浏览器的执行结果也需要通过浏览器内核反馈给代码

这个原生的类的实例化就是driver驱动实例化:

1)发送请求:把调用的方法封装成符合Webdriver协议的请求发给浏览器内核

2)接受结果:反之也能反馈浏览器操作结果给代码,是浏览器内核和代码之间的桥梁

3)管理会话:从driver实例化(创建浏览器会话)到driver.quit()关闭浏览器结束会话,整个浏览器的生命周期由driver统一管理。

python 复制代码
class WebDriverExt(webdriver.Remote):

WebDriverExt 继承了 webdriver.Remote,说明框架:

让代码可以跨机器、跨浏览器地控制网页,能大规模自动化测试

2. __init__ 方法:初始化扩展类
python 复制代码
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 来管理浏览器。
python 复制代码
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,确保兼容性。
python 复制代码
    super(WebDriverExt, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive,
                                       file_detector)
  • super(子类名, self).init(参数):调用父类的构造方法,确保父类的初始化逻辑被执行(如连接远程服务器、初始化浏览器)。
Matlab 复制代码
    self._driver = self  # 给当前实例(self)起一个别名_driver
  • self._driver:定义实例属性 _driver,值为实例本身(self)。

  • 用途:后续在类内部可以通过 self._driver 调用实例的方法(和 self 效果一样,可能是为了代码可读性或兼容旧逻辑)。

    复制代码
      self.initConfig()  # 调用当前类的initConfig方法,执行自定义配置
  • self.initConfig():调用本类中定义的 initConfig 方法,触发额外的初始化逻辑(如设置等待时间)。

3. initConfig 方法:设置默认配置
python 复制代码
def initConfig(self):
    """初始化Driver的配置"""
    # 从框架配置中读取"隐式等待时间",设置给driver
    self.implicitly_wait(settings.DRIVER['implicitlyWait'])
  • implicitly_wait() 是 Selenium 的隐式等待方法(当查找元素时,如果没找到,会等待指定时间再报错)。
  • settings.DRIVER['implicitlyWait']:从框架的配置文件(settings)中读取预设的等待时间(比如 10 秒),避免每次使用都手动设置,实现 "全局统一配置"。
4. windowScrollTo 方法:自定义页面滚动功能
python 复制代码
def windowScrollTo(self, x, y):
    # 确保滚动坐标不小于0(避免无效值)
    x = 0 if x < 0 else x
    y = 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"增强版的浏览器驱动"

  1. 保留了 Selenium 原生的所有功能(继承自 webdriver.Remote);
  2. 自动应用框架的全局配置(如隐式等待时间,通过 initConfig 实现);
  3. 新增了常用的自定义操作(如 windowScrollTo 封装页面滚动)。

二、类

1. 类定义:继承 WebDriverExt
python 复制代码
class WebDriver(WebDriverExt):  # 定义WebDriver类,继承自WebDriverExt
2. __init__ 方法:初始化并新增基础属性
python 复制代码
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()、切换到 iframe driver._switch_to.frame())。
    • 这里在 WebDriver 中初始化 SwitchTo 并绑定当前实例,意味着用例中创建 WebDriver 对象后,能直接用 driver._switch_to 做切换操作,无需手动初始化。

⭐ 分开的作用:职责分离

  • WebDriverExt:负责 通用基础增强(如全局隐式等待配置、通用页面滚动),是所有项目都能复用的 "基础层"。
  • WebDriver:负责 专项功能扩展 (如窗口切换、CDP 命令、自定义 Cookie 操作),是当前项目特有的 "业务层"。

三、SwitchTo 类

对 Selenium 原生 SwitchTo 类的 定制化扩展 ,主要用于更便捷、更健壮地处理 窗口切换、表单切换、弹窗(Alert)操作。它在保留原生功能的基础上,增加了对 "窗口索引切换" 的支持和自定义异常处理,让测试用例中的切换操作更符合实际使用习惯。

python 复制代码
class SwitchTo(SeleniumSwitchTo):  # 自定义SwitchTo类,继承自Selenium原生SwitchTo
python 复制代码
def __init__(self, driver):
    super(SwitchTo, self).__init__(driver)  # 调用父类初始化方法,保留原生初始化逻辑
    self._driver = driver  # 保存驱动实例到当前类,后续切换操作可能需要用到driver

window 方法:窗口切换(核心扩展功能)

这是整个类最关键的增强点 ------ 原生 window() 方法只能通过 "窗口句柄(字符串)" 切换,而框架扩展后支持 "整数索引" 切换,更符合测试人员的使用习惯。

python 复制代码
#@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 类)

python 复制代码
@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

python 复制代码
# coding=utf-8  # 声明编码格式,支持中文注释(避免中文乱码)
from selenium.webdriver.common.alert import Alert as SeleniumAlert  # 导入Selenium原生Alert类,起别名"SeleniumAlert"
  • 关键:用 as SeleniumAlert 给原生类起别名,是为了 避免和当前自定义的 Alert 类重名 (如果直接 from ... import Alert,会覆盖自定义类,导致报错)。
python 复制代码
class Alert(SeleniumAlert):  # 自定义Alert类,继承自Selenium原生Alert
  • 语法:class 子类(父类),这是 Python 面向对象 "继承" 的基础写法,确保自定义类能复用原生类的所有方法(如 accept()dismiss() 等)。
python 复制代码
def __init__(self, driver):
    super(Alert, self).__init__(driver)  # 调用父类(原生Alert)的构造函数
    self._driver = driver  # 额外保存驱动实例到当前类
dismiss 方法:关闭弹窗(预留扩展位)
python 复制代码
#@driverAction_dec("取消警告框",False)  # 注释掉的装饰器(核心预留点)
def dismiss(self):
    super(Alert, self).dismiss()  # 直接调用原生Alert的dismiss()方法
  • 原生功能:dismiss() 用于 "关闭弹窗"(对应弹窗的 "取消" 按钮,比如浏览器提示 "是否离开此页面" 时,点击 "取消")。

  • 框架封装意图:目前完全复用原生逻辑,没有新增代码,但注释的 @driverAction_dec 装饰器是关键 ------ 这是 预留的扩展位
    比如未来想给 "关闭弹窗" 操作添加日志(记录 "何时关闭了弹窗")或截图(关闭前截图留存证据),只需启用装饰器。

    装饰器 driverAction_dec 是框架自定义的工具,能自动添加日志、截图等通用增强功能,无需修改 dismiss() 本身的代码。

accept 方法:确认弹窗(同 dismiss 逻辑)
python 复制代码
#@driverAction_dec("接受警告框",False)  # 同样是预留装饰器
def accept(self):
    super(Alert, self).accept()  # 调用原生Alert的accept()方法
send_keys 方法:向弹窗输入文本(同逻辑)
python 复制代码
#@driverAction_dec("发送文本到警告框",False)  # 预留装饰器
def send_keys(self, keysToSend):
    super(Alert, self).send_keys(keysToSend)  # 调用原生Alert的send_keys()方法

表面看,这个类只是 "重复调用原生方法",似乎没什么用,但实际是框架设计中 "预留扩展、统一管控" 思想的体现:

  1. 预留扩展空间:通过注释的装饰器,未来无需修改核心代码,就能快速给弹窗操作添加日志、截图、重试等增强功能(符合 "开闭原则":对扩展开放,对修改关闭);
  2. 统一 API 风格 :框架的 SwitchToWebDriverExt 等类都采用 "继承原生类 + 轻量封装" 的模式,Alert 类保持一致风格,方便测试人员记忆和使用(比如用 driver._switch_to.alert.accept() 确认弹窗,符合框架统一逻辑);
  3. 便于业务定制 :如果未来项目有特殊弹窗需求(比如 "弹窗文本必须包含 XX 关键词才确认"),可以直接在这些方法中添加业务逻辑。

五、By

这个 WebDriverBy 类是框架中的 "元素定位方式映射转换器" ,核心作用是:将框架 自定义的 By 类型 (来自 frameworkCore.driver.by),统一转换为 Selenium 原生的 By 类型(来自 selenium.webdriver.common.by),解决 "框架自定义定位标识" 与 "Selenium 原生定位标识" 的兼容问题。

python 复制代码
# 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 重名,会导致代码混淆)。
python 复制代码
class WebDriverBy:  # 无父类,是一个"纯静态工具类"(只存映射表和转换方法)
  • 这个类不需要继承任何父类,因为它的核心功能是 "存储映射关系" 和 "提供转换方法",本质是一个 工具类(类似 "字典 + 静态方法" 的组合,但用类封装更规整)。
类属性 byMap:定位方式映射表
python 复制代码
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:执行定位方式转换
python 复制代码
@classmethod  # 装饰为"类方法",无需创建实例,直接通过类调用
def convert_by(cls, by):
    return cls.byMap[by]  # 根据传入的框架By,返回对应的原生By

WebDriverBy 就是框架和 Selenium 之间的 "翻译官"------ 让框架的自定义定位标识,能被 Selenium 看懂并执行~

六、 exceptionUtil模块

单独创建 exceptionUtil 类(或工具模块),核心目的是 "统一异常判断逻辑,实现异常处理的复用与解耦"------ 把 "判断是否为超时异常" 这类通用逻辑抽成独立工具,避免在框架各个角落重复写相同代码,同时让异常判断更规范、更易维护。

模块直接定义了函数 isTimeOutException,本质是一个 "异常处理工具模块" (命名为 exceptionUtil.py),而非严格意义上的 "类"。这种设计更轻量,适合封装单一、通用的异常判断逻辑。

python 复制代码
from frameworkCore.driver.exception import TimeOutException, TimeOutException
python 复制代码
def isTimeOutException(err):
    return isinstance(err, TimeOutException)
  • 核心功能:判断传入的异常对象 err,是否属于 TimeOutException 类型(或其子类类型)。

  • 关键语法:isinstance(err, 异常类) 是 Python 中判断 "对象是否属于某个类(或其派生类)" 的标准方法,返回 True/False

    • 举例:如果 errTimeOutException("元素定位超时") 的实例,返回 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 次:

python 复制代码
import exceptionUtil  # 导入异常工具
from frameworkCore.driver.exception import TimeOutException

def 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:日志记录异常类型

在框架的日志模块中,根据异常类型输出不同级别日志:

python 复制代码
import logging
import exceptionUtil

def 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,不要重复造轮子)" 的典型体现~

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习