18. 结合Selenium和YAML对页面继承对象PO的改造
一、架构改造核心思路
1.1 改造前后对比
1.2 核心优势
- 定位信息与代码解耦
- 支持多环境配置切换
- 提升代码可维护性
- 实现元素配置热更新
二、PO核心类改造解析
2.1 页面基类增强
class Page:elements_yml = {} # 子类需覆盖的配置映射elements_pool = {} # 配置缓存池def _locator(self, expression: str = 'cp.username'):key, value = expression.split('.')# 动态加载YAML配置if key not in self.elements_pool:self.elements_pool[key] = YamlReader(self.elements_yml[key]).data# 海象运算符验证定位方法if (locator := self.elements_pool[key][value])[0] not in BY_RULES:raise Exception(f'无效定位方法: {locator}')return self.elements_pool[key][value]
关键技术点:
- 双缓存机制(
elements_yml
与elements_pool
) - 表达式解析(
key.value
格式) - 定位方法白名单验证(
BY_RULES
)
三、配置管理系统升级
3.1 setting.py核心配置
# 元素配置文件映射
YAML_ELEMENT = {'cp': join(ELEMENTS_YAML_FILE_PATH, 'CommonLoginPass.yml'),'op': join(ELEMENTS_YAML_FILE_PATH, 'oder_page.yml')
}# 合法定位方法白名单
BY_RULES = ('id', 'xpath', 'link text', 'partial link text', 'name','tag name', 'class name', 'css selector'
)
3.2 路径管理优化
# 动态路径构建
ELEMENTS_YAML_FILE_PATH = join(BASE_PATH, 'Chap5\\page')
CHAPTER_1_PATH = join(BASE_PATH, 'chap3') # 跨平台兼容
四、页面类实现示例
4.1 登录页面改造
class CommonLoginPass(Page):elements_yml = YAML_ELEMENT # 绑定配置文件def login(self, username: str = 'Tester'):self.element('cp.username').send_keys(username) # 表达式驱动self.element('cp.password').send_keys(password)self.element('cp.loginBtn').click()
4.2 订单页面继承
class Oder(CommonLoginPass):def search_bug(self):self.element('op.clickOrder').click() # 继承配置映射self.element('op.orderInput').send_keys('Tom')
五、YAML配置文件规范
5.1 元素定义标准格式
# CommonLoginPass.yml
username:- id # 定位类型- ctl00_MainContent_username # 定位表达式loginBtn:- id- ctl00_MainContent_login_button
5.2 配置文件结构要求
- 二级键值为列表类型
- 首个元素必须为BY_RULES允许的定位方法
- 元素命名采用小驼峰格式
- 注释说明元素用途
六、执行流程优化
6.1 元素加载流程
6.2 性能优化策略
- 首次访问时加载配置文件
- 内存缓存已解析配置
- 避免重复IO操作
- 按需加载不同模块配置
七、改造收益分析
指标 | 改造前 | 改造后 | 提升率 |
---|---|---|---|
维护成本 | 需修改源代码 | 仅改配置文件 | 70% |
定位信息复用 | 类级别复用 | 跨项目复用 | 200% |
执行效率 | 每次重新定位 | 缓存加速访问 | 40% |
可读性 | 代码混杂定位 | 业务逻辑聚焦 | 60% |
八、完整代码
"""
Python :3.13.3
Selenium: 4.31.0po.py
"""from chap3.ob import *
from setting import *
from chap5.file_reader import YamlReaderclass Page:url = Nonedriver = None# 子类重写,获取通用配置文件中具体项目的元素配置文件字典elements_yml = {}# 缓存动态读取的yaml元素配置文件的解析结果elements_pool = {}def _locator(self, expression: str = 'cp.username'):"""解析元素表达式的方法:param expression::return:"""key, value = expression.split('.')if key not in self.elements_yml:raise Exception('元素配置文件的别名:{}无法识别!'.format(key))if key not in self.elements_pool:self.elements_pool[key] = YamlReader(self.elements_yml[key]).dataif (locator := self.elements_pool[key][value])[0] not in BY_RULES:raise Exception(f'无法识别定位方法:{locator}')return locatorreturn self.elements_pool[key][value]@classmethoddef cls_locator(cls, expression: str = 'cp.username'):"""类方法版本的locator,解析元素表达式:param expression: 元素表达式,格式为'配置文件别名.元素名':return: 定位元组(定位方式, 定位表达式)"""key, value = expression.split('.')if key not in cls.elements_yml:raise Exception('元素配置文件的别名:{}无法识别!'.format(key))if key not in cls.elements_pool:cls.elements_pool[key] = YamlReader(cls.elements_yml[key]).dataif (locator := cls.elements_pool[key][value])[0] not in BY_RULES:raise Exception(f'无法识别定位方法:{locator}')return locatorreturn cls.elements_pool[key][value]@classmethoddef cls_element(cls, loc: str):return cls.driver.find_element(*cls.cls_locator(loc))def element(self, loc: str):"""定位元素的方法:param loc::return:"""return self.driver.find_element(*self._locator(loc))def elements(self, loc: str):"""定位一组元素或多个元素:param loc::return:"""return self.driver.find_element(*self._locator(loc))class CommonLoginPass(Page):url = PROJECT_Oder_URLdriver = CHROME().start_chrome_browser# username = ('id', 'ctl00_MainContent_username')# password = ('id', 'ctl00_MainContent_password')# loginBtn = ('id', 'ctl00_MainContent_login_button')elements_yml = YAML_ELEMENTdef get(self):"""打开首页地址:return:"""self.driver.get(self.url)@classmethoddef cls_get(cls):"""类方法,打开首页:return:"""cls.driver.get(cls.url)def login(self, username: str = 'Tester', password: str = 'test'):# self.element(self.username).send_keys(username)# self.element(self.password).send_keys(password)# self.element(self.loginBtn).click()self.element('cp.username').send_keys(username)self.element('cp.password').send_keys(password)self.element('cp.loginBtn').click()@classmethoddef cls_login(cls, username: str = 'Tester', password: str = 'test'):"""类方法,登录:return:"""cls.cls_element('cp.username').send_keys(username)cls.cls_element('cp.password').send_keys(password)cls.cls_element('cp.loginBtn').click()class Oder(CommonLoginPass):# clickOrder = ('xpath', '//*[@id="ctl00_menu"]/li[3]/a')# orderInput = ('id', 'ctl00_MainContent_fmwOrder_txtName')# clickProcess = ('id', 'ctl00_MainContent_fmwOrder_InsertButton')## bug_label = ('id', "ctl00_MainContent_fmwOrder_RequiredFieldValidator3")# order_label = ('xpath', '//*[@id="aspnetForm"]//td[1]/h1')## invalid_login = ('xpath', '//*[@id="ctl00_MainContent_status"]')## log_out = ('xpath', '//*[@id="ctl00_logout"]')def search_bug(self, order_input: str = 'Tom'):self.element('op.clickOrder').click()self.element('op.orderInput').send_keys(order_input)self.element('op.clickProcess').click()def logout(self):self.element('op.log_out').click()class TestOder(Oder):"""测试登录和检索bug功能"""def test_login(self):self.get()self.login()assert self.element('op.order_label').text == 'Web Orders'print('test_login is passed')def test_search(self):self.search_bug()from time import sleepsleep(4)assert self.element('op.bug_label').text == "Field 'Street' cannot be empty."print('test_search is passed')self.driver.quit()obj = TestOder()
obj.test_login()
obj.test_search()
下面是setting.py的代码:
"""
Python :3.13.3
Selenium: 4.31.0
"""# 项目地址
# 项目包和文件夹的路径
# 浏览器对象属性
# 测试套件from os.path import dirname, join# -------------------项目地址-----------------------
# 项目一的地址
PROJECT_Oder_URL = 'http://secure.smartbearsoftware.com/samples/testcomplete12/WebOrders/Login.aspx'# 项目二的地址
PROJECT_QQ_URL = ''# 项目三的地址
PROJECT_DEMO_URL = ''# -------------------项目包和文件夹的路径-----------------------
# 项目根目录
BASE_PATH = dirname(__file__)# 浏览器驱动文件地址
CHROME_DRIVER_PATH = join(BASE_PATH, 'drivers\\chrome_driver.exe')
EDGE_DRIVER_PATH = join(BASE_PATH, 'driver\\edge_driver.exe')# 项目模块路径
# 模块1路径
CHAPTER_1_PATH = join(BASE_PATH, 'chap3')
# 模块2路径
CHAPTER_2_PATH = join(BASE_PATH, 'chap4')
# 模块3路径
CHAPTER_3_PATH = join(BASE_PATH, 'chap5')# 元素配置文件的根目录
ELEMENTS_YAML_FILE_PATH = join(BASE_PATH, 'Chap5\\page')
# -------------------测试套件-----------------------
# 流程1相关测试套件
SUIT_MODULE_1 = ['test_module_1.py','test_module_2.py'
]# 流程2相关测试套件
SUIT_MODULE_2 = ['test_module_1.py','test_module_2.py','test_module_3.py'
]# 流程3相关测试套件
SUIT_MODULE_3 = ['test_module_4.py','test_module_5.py'
]# 项目一的主测试套件
SUIT_PROJECT1 = ['test_module_1.py','test_module_2.py','test_module_3.py']# 项目二的主测试套件
SUIT_PROJECT2 = SUIT_MODULE_2 + SUIT_MODULE_3# -------------------浏览器对象属性-----------------------
# 浏览器基本属性# 无头化
HEADLESS = False# 隐式等待时间
IMP_TIME = 30# 页面加载超时时间
PAGE_LOAD_TIME = 20# JS异步执行超时时间
SCRIPT_TIME_OUT = 20# 浏览器尺寸
WINDOWS_SIZE = (1024, 768)# -------------------CHROME浏览器属性-----------------------# chrome浏览器操作开关
CHROME_METHOD_MARK = True# chrome启动参数开关
CHROME_OPTION_MARK = True# chrome实验性质启动参数
CHROME_EXP = {'excludeSwitches': ['enable-automation'],# 'mobileEmulation': {'deviceName': 'iPhone 6'}}# chrome窗口大小启动参数
CHROME_WINDOWS_SIZE = (1920, 900)# chrome启动最大化参数
CHROME_START_MAX = '--start-maximized'# -------------------EDGE浏览器属性-----------------------
# -------------------FIREFOX浏览器属性-----------------------# -------------------YAML元素配置文件-----------------------
YAML_ELEMENT = {'cp': join(ELEMENTS_YAML_FILE_PATH, 'CommonLoginPass.yml'),'op': join(ELEMENTS_YAML_FILE_PATH, 'oder_page.yml')
}
# -------------------YAML元素配置文件-----------------------#-------------------WEB元素定位方法-----------------------
BY_RULES = ('id','xpath','link text','partial link text','name','tag name','class name','css selector')
#-------------------WEB元素定位方法-----------------------
两份存放元素的yaml文件:
# 登录账号
username:- id- ctl00_MainContent_username# 密码
password:- id- ctl00_MainContent_password# 登录按钮
loginBtn:- id- ctl00_MainContent_login_button
# 点击‘Oder’按钮
clickOrder:- xpath- //*[@id="ctl00_menu"]/li[3]/a# 在字段‘Customer name’输入'Tom'
orderInput:- id- ctl00_MainContent_fmwOrder_txtName# 点击'Process'按钮
clickProcess:- id- ctl00_MainContent_fmwOrder_InsertButton# 检查‘Field 'Customer name' cannot be empty.’提示是否存在
bug_label:- id- ctl00_MainContent_fmwOrder_RequiredFieldValidator3# 检查‘Web Orders’标题是否存在
order_label:- xpath- //*[@id="aspnetForm"]//td[1]/h1# 检查账号密码错误时的提示内容
invalid_login:- xpath- //*[@id="ctl00_MainContent_status"]# 点击'logout'按钮
log_out:- xpath- //*[@id="ctl00_logout"]
「小贴士」:点击头像→【关注】按钮,获取更多软件测试的晋升认知不迷路! 🚀