18. 结合Selenium和YAML对页面继承对象PO的改造
18. 结合Selenium和YAML对页面继承对象PO的改造
一、架构改造核心思路
1.1 改造前后对比
#mermaid-svg-ziagMhNLS5fIFWrx {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ziagMhNLS5fIFWrx .error-icon{fill:#552222;}#mermaid-svg-ziagMhNLS5fIFWrx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ziagMhNLS5fIFWrx .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-ziagMhNLS5fIFWrx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ziagMhNLS5fIFWrx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ziagMhNLS5fIFWrx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ziagMhNLS5fIFWrx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ziagMhNLS5fIFWrx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ziagMhNLS5fIFWrx .marker.cross{stroke:#333333;}#mermaid-svg-ziagMhNLS5fIFWrx svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ziagMhNLS5fIFWrx .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ziagMhNLS5fIFWrx .cluster-label text{fill:#333;}#mermaid-svg-ziagMhNLS5fIFWrx .cluster-label span{color:#333;}#mermaid-svg-ziagMhNLS5fIFWrx .label text,#mermaid-svg-ziagMhNLS5fIFWrx span{fill:#333;color:#333;}#mermaid-svg-ziagMhNLS5fIFWrx .node rect,#mermaid-svg-ziagMhNLS5fIFWrx .node circle,#mermaid-svg-ziagMhNLS5fIFWrx .node ellipse,#mermaid-svg-ziagMhNLS5fIFWrx .node polygon,#mermaid-svg-ziagMhNLS5fIFWrx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ziagMhNLS5fIFWrx .node .label{text-align:center;}#mermaid-svg-ziagMhNLS5fIFWrx .node.clickable{cursor:pointer;}#mermaid-svg-ziagMhNLS5fIFWrx .arrowheadPath{fill:#333333;}#mermaid-svg-ziagMhNLS5fIFWrx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ziagMhNLS5fIFWrx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ziagMhNLS5fIFWrx .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-ziagMhNLS5fIFWrx .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-ziagMhNLS5fIFWrx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ziagMhNLS5fIFWrx .cluster text{fill:#333;}#mermaid-svg-ziagMhNLS5fIFWrx .cluster span{color:#333;}#mermaid-svg-ziagMhNLS5fIFWrx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ziagMhNLS5fIFWrx :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}硬编码元素定位YAML配置驱动原始PO模式维护困难改造后PO模式动态加载元素
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.ymlusername: - id # 定位类型 - ctl00_MainContent_username # 定位表达式loginBtn: - id - ctl00_MainContent_login_button
5.2 配置文件结构要求
- 二级键值为列表类型
- 首个元素必须为BY_RULES允许的定位方法
- 元素命名采用小驼峰格式
- 注释说明元素用途
六、执行流程优化
6.1 元素加载流程
#mermaid-svg-2WbyJOsbQWY4gHUE {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-2WbyJOsbQWY4gHUE .error-icon{fill:#552222;}#mermaid-svg-2WbyJOsbQWY4gHUE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2WbyJOsbQWY4gHUE .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-2WbyJOsbQWY4gHUE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2WbyJOsbQWY4gHUE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2WbyJOsbQWY4gHUE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2WbyJOsbQWY4gHUE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2WbyJOsbQWY4gHUE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2WbyJOsbQWY4gHUE .marker.cross{stroke:#333333;}#mermaid-svg-2WbyJOsbQWY4gHUE svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2WbyJOsbQWY4gHUE .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2WbyJOsbQWY4gHUE text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2WbyJOsbQWY4gHUE .actor-line{stroke:grey;}#mermaid-svg-2WbyJOsbQWY4gHUE .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2WbyJOsbQWY4gHUE .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2WbyJOsbQWY4gHUE #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2WbyJOsbQWY4gHUE .sequenceNumber{fill:white;}#mermaid-svg-2WbyJOsbQWY4gHUE #sequencenumber{fill:#333;}#mermaid-svg-2WbyJOsbQWY4gHUE #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2WbyJOsbQWY4gHUE .messageText{fill:#333;stroke:#333;}#mermaid-svg-2WbyJOsbQWY4gHUE .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2WbyJOsbQWY4gHUE .labelText,#mermaid-svg-2WbyJOsbQWY4gHUE .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2WbyJOsbQWY4gHUE .loopText,#mermaid-svg-2WbyJOsbQWY4gHUE .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2WbyJOsbQWY4gHUE .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-2WbyJOsbQWY4gHUE .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2WbyJOsbQWY4gHUE .noteText,#mermaid-svg-2WbyJOsbQWY4gHUE .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2WbyJOsbQWY4gHUE .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2WbyJOsbQWY4gHUE .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2WbyJOsbQWY4gHUE .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2WbyJOsbQWY4gHUE .actorPopupMenu{position:absolute;}#mermaid-svg-2WbyJOsbQWY4gHUE .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-2WbyJOsbQWY4gHUE .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2WbyJOsbQWY4gHUE .actor-man circle,#mermaid-svg-2WbyJOsbQWY4gHUE line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2WbyJOsbQWY4gHUE :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}PageObjectYamlReaderSeleniumYAML文件解析表达式(cp.username)加载CommonLoginPass.yml返回配置数据缓存配置driver.find_element(id, ctl00...)PageObjectYamlReaderSeleniumYAML文件
6.2 性能优化策略
- 首次访问时加载配置文件
- 内存缓存已解析配置
- 避免重复IO操作
- 按需加载不同模块配置
七、改造收益分析
八、完整代码
\"\"\"Python :3.13.3Selenium: 4.31.0po.py\"\"\"from chap3.ob import *from setting import *from chap5.file_reader import YamlReaderclass Page: url = None driver = 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]).data if (locator := self.elements_pool[key][value])[0] not in BY_RULES: raise Exception( f\'无法识别定位方法:{locator}\' ) return locator return self.elements_pool[key][value] @classmethod def 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]).data if (locator := cls.elements_pool[key][value])[0] not in BY_RULES: raise Exception( f\'无法识别定位方法:{locator}\' ) return locator return cls.elements_pool[key][value] @classmethod def 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_URL driver = CHROME().start_chrome_browser # username = (\'id\', \'ctl00_MainContent_username\') # password = (\'id\', \'ctl00_MainContent_password\') # loginBtn = (\'id\', \'ctl00_MainContent_login_button\') elements_yml = YAML_ELEMENT def get(self): \"\"\" 打开首页地址 :return: \"\"\" self.driver.get(self.url) @classmethod def 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() @classmethod def 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 sleep sleep(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.3Selenium: 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\"]
「小贴士」:点击头像→【关注】按钮,获取更多软件测试的晋升认知不迷路! 🚀