19. 结合Selenium和YAML对页面实例化PO对象改造_selenium+yaml
19. 结合Selenium和YAML对页面实例化PO对象改造
一、架构升级核心思路
1.1 改造核心目标
# 原始PO模式:显式定义元素定位username = (\'id\', \'ctl00_MainContent_username\')# 改造后PO模式:动态属性访问self.username.send_keys(\'Tester\') # 自动触发元素定位
1.2 关键技术实现
- 元编程技术:通过
__getattr__
实现动态属性访问 - 配置驱动模式:YAML文件存储元素定位策略
- 链式继承体系:实现跨页面元素复用
二、核心类改造解析
2.1 页面基类增强
class Page: locators = {} # 元素定位池 browser = CHROME # 浏览器类型绑定 def __getattr__(self, loc): \"\"\"动态属性访问拦截器\"\"\" if loc not in self.locators: raise AttributeError(f\"\'{self.__class__.__name__}\'未定义元素\'{loc}\'\") by, val = self.locators[loc] # 解构定位策略 return self.driver.find_element(by, val) # 延迟定位执行
核心机制:
- 按需定位:元素首次访问时执行定位
- 异常封装:自动抛出可读性错误
- 驱动管理:统一浏览器实例生命周期
三、配置管理系统升级
3.1 setting.py核心配置
# YAML元素配置文件映射YAML_ELEMENT = { \'cp\': join(ELEMENTS_PATH, \'CommonLoginPass.yml\'), \'op\': join(ELEMENTS_PATH, \'oder_page.yml\')}# 浏览器启动参数CHROME_EXP = { \'excludeSwitches\': [\'enable-automation\'], \'mobileEmulation\': {\'deviceName\': \'iPhone 12\'}}
3.2 配置加载方式
class CommonLoginPage(Page): locators = YamlReader(YAML_ELEMENT[\'cp\']).data # 动态加载登录页配置class MainPage(CommonLoginPage): locators.update(YamlReader(YAML_ELEMENT[\'op\']).data) # 继承并扩展配置
四、页面类实现模式
4.1 登录页面实现
class CommonLoginPage(Page): url = PROJECT_Oder_URL def login(self, username=\'Tester\'): self.driver.get(self.url) self.username.send_keys(username) # 动态属性访问 self.password.send_keys(\'test\') self.loginBtn.click()
4.2 主页面扩展
class MainPage(CommonLoginPage): def search_bug(self): self.clickOrder.click() # 继承父类配置 self.orderInput.send_keys(\'Tom\') # 新增子类配置
五、执行流程优化
5.1 元素定位流程
#mermaid-svg-AVP5wEZZSb79wIvb {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-AVP5wEZZSb79wIvb .error-icon{fill:#552222;}#mermaid-svg-AVP5wEZZSb79wIvb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AVP5wEZZSb79wIvb .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-AVP5wEZZSb79wIvb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AVP5wEZZSb79wIvb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AVP5wEZZSb79wIvb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AVP5wEZZSb79wIvb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AVP5wEZZSb79wIvb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AVP5wEZZSb79wIvb .marker.cross{stroke:#333333;}#mermaid-svg-AVP5wEZZSb79wIvb svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AVP5wEZZSb79wIvb .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AVP5wEZZSb79wIvb text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-AVP5wEZZSb79wIvb .actor-line{stroke:grey;}#mermaid-svg-AVP5wEZZSb79wIvb .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-AVP5wEZZSb79wIvb .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-AVP5wEZZSb79wIvb #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-AVP5wEZZSb79wIvb .sequenceNumber{fill:white;}#mermaid-svg-AVP5wEZZSb79wIvb #sequencenumber{fill:#333;}#mermaid-svg-AVP5wEZZSb79wIvb #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-AVP5wEZZSb79wIvb .messageText{fill:#333;stroke:#333;}#mermaid-svg-AVP5wEZZSb79wIvb .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AVP5wEZZSb79wIvb .labelText,#mermaid-svg-AVP5wEZZSb79wIvb .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-AVP5wEZZSb79wIvb .loopText,#mermaid-svg-AVP5wEZZSb79wIvb .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-AVP5wEZZSb79wIvb .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-AVP5wEZZSb79wIvb .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-AVP5wEZZSb79wIvb .noteText,#mermaid-svg-AVP5wEZZSb79wIvb .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-AVP5wEZZSb79wIvb .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AVP5wEZZSb79wIvb .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AVP5wEZZSb79wIvb .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AVP5wEZZSb79wIvb .actorPopupMenu{position:absolute;}#mermaid-svg-AVP5wEZZSb79wIvb .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-AVP5wEZZSb79wIvb .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AVP5wEZZSb79wIvb .actor-man circle,#mermaid-svg-AVP5wEZZSb79wIvb line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-AVP5wEZZSb79wIvb :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}TestCasePageObjectYAMLBrowser访问page.username检查locators缓存返回定位策略find_element(by,value)WebElement对象TestCasePageObjectYAMLBrowser
5.2 浏览器管理优化
def __init__(self, page=None): if page: # 支持页面间共享driver self.driver = page.driver else: # 新建浏览器实例 self.driver = self.browser().start_chrome_browser
六、改造收益分析
6.1 技术指标对比
6.2 工程实践优势
- 配置热更新:修改YAML文件无需重启测试
- 环境隔离:通过不同YAML配置支持多环境
- 元素版本化:配合Git管理定位策略变更
- 团队协作:前端与测试并行开发
七、最佳实践指南
7.1 YAML规范建议
loginBtn: - id # 定位类型 - ctl00_login_button # 定位值 - desc: 登录按钮 # 元数据扩展 - timeout: 10 # 显式等待参数
7.2 异常处理增强
def __getattr__(self, loc): try: by, val = self.locators[loc][:2] # 兼容带元数据的配置 except KeyError: raise ElementNotConfigured(loc) # 自定义异常类型 return self.wait.until(EC.presence_of_element_located((by, val)))
八、完整代码
\"\"\"Python :3.13.3Selenium: 4.31.0po_2.py\"\"\"from chap3.ob import *from setting import *from chap5.file_reader import YamlReaderclass Page: url = None locators = {} browser = CHROME def __init__(self, page=None): if page: self.driver = page.driver else: self.driver = self.browser().start_chrome_browser def __getattr__(self, loc): if loc not in self.locators.keys(): raise Exception by, val = self.locators[loc] return self.driver.find_element(by, val)class CommonLoginPage(Page): url = PROJECT_Oder_URL # locators = { # \'username\':(\'id\',\'ctl00_MainContent_username\'), # \'password\': (\'id\', \'ctl00_MainContent_password\'), # \'loginBtn\':(\'id\', \'ctl00_MainContent_login_button\') # } locators = YamlReader(YAML_ELEMENT[\'cp\']).data def get(self): \"\"\" 打开首页地址 :return: \"\"\" self.driver.get(self.url) def login(self, username: str = \'Tester\', password: str = \'test\'): self.username.send_keys(username) self.password.send_keys(password) self.loginBtn.click()class MainPage(CommonLoginPage): # CommonLoginPage.locators.update({ # \'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\') # }) CommonLoginPage.locators.update( YamlReader(YAML_ELEMENT[\'op\']).data ) def search_bug(self, order_input: str = \'Tom\'): self.clickOrder.click() self.orderInput.send_keys(order_input) self.clickProcess.click()class TestMain: \"\"\" 测试登录和检索bug功能 \"\"\" def test_login(self): page = MainPage() page.get() page.login() assert page.order_label.text == \'Web Orders\' print(\'test_login is passed\') page.driver.quit() def test_search(self): page = MainPage() page.get() page.login() page.search_bug() from time import sleep sleep(4) assert page.bug_label.text == \"Field \'Street\' cannot be empty.\" print(\'test_search is passed\') page.driver.quit()
「小贴士」:点击头像→【关注】按钮,获取更多软件测试的晋升认知不迷路! 🚀