课外活动:再次理解页面实例化PO对象的魔法方法__getattr__
一、动态属性访问机制解析
1.1 核心实现原理
class Page:
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) # 动态返回WebElement
执行流程分析:
二、self.username的生成路径
2.1 配置加载阶段
class CommonLoginPage(Page):
locators = YamlReader(YAML_ELEMENT['cp']).data # 加载YAML配置
YAML配置文件示例:
username:
- id
- ctl00_MainContent_username
password:
- id
- ctl00_MainContent_password
loginBtn:
- id
- ctl00_MainContent_login_button
2.2 属性访问阶段
def login(self, username: str = 'Tester'):
self.username.send_keys(username) # 触发__getattr__
具体执行步骤:
- 解释器检查
self
对象是否存在username
属性 - 未找到属性时调用
__getattr__('username')
- 在
locators
字典中查找’username’键 - 获取元组
('id', 'ctl00_MainContent_username')
- 执行
driver.find_element(id, 'ctl00_MainContent_username')
- 返回WebElement对象
- 调用该对象的
send_keys()
方法
三、关键技术点解析
3.1 延迟加载机制
# 仅在首次访问时加载配置
if loc not in self.locators.keys():
raise Exception # 即时失败机制
优势特征:
- 避免启动时加载全部元素
- 减少不必要的资源消耗
- 支持动态配置更新
- 提升测试执行效率
3.2 异常处理流程
四、设计模式优势分析
4.1 与传统写法的对比
传统写法 | 实例化PO写法 |
---|---|
self.find_element(id, '...') | self.username |
显式维护定位器字典 | YAML配置自动加载 |
需要重复编写find_element | 通过魔法方法自动处理 |
修改定位策略需要修改代码 | 仅需修改YAML文件 |
4.2 工程化优势
- 关注点分离:测试逻辑与元素定位解耦
- 维护成本降低:修改元素定位无需改动代码
- 可读性提升:业务逻辑更直观
- 扩展性增强:支持多环境配置切换
五、注意事项与最佳实践
5.1 配置规范要求
# 正确写法(严格缩进)
loginBtn:
- id
- ctl00_login_button
# 错误写法(缺少层级)
loginBtn: id, ctl00_login_button
5.2 异常处理建议
def __getattr__(self, loc):
try:
by, val = self.locators[loc]
except KeyError:
raise ElementNotFoundError(f"元素'{loc}'未配置") # 自定义异常
except ValueError:
raise InvalidLocatorError(f"定位器格式错误") # 配置校验
return self.driver.find_element(by, val)
六、性能优化方向
6.1 元素缓存机制
class Page:
_elements_cache = {} # 新增元素缓存
def __getattr__(self, loc):
if loc not in self._elements_cache:
# ...定位逻辑...
self._elements_cache[loc] = element
return self._elements_cache[loc]
6.2 预加载策略
def preload_elements(self):
for loc in self.locators:
getattr(self, loc) # 提前触发元素加载
七、完整代码
"""
Python :3.13.3
Selenium: 4.31.0
"""
from chap3.ob import *
from setting import *
from chap5.file_reader import YamlReader
class 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()
obj = TestMain()
obj.test_login()
obj.test_search()
「小贴士」:点击头像→【关注】按钮,获取更多软件测试的晋升认知不迷路! 🚀