【实战】基于pageobject模式的测试框架优化实战
基于 Page Object 模式的测试框架优化实战
需求说明
- 被测应用:雪球 App,请在应用商店直接安装。
-
雪球 App 介绍
- 雪球 app 是一款免费版,非常实用的股票平台,提供热点资讯内容和实时行情,提供了证券交流交易,公募、理财服务
- 雪球 app 功能包括搜索股票,查看行情,交易,浏览热门文章,发帖,登录,注册,等功能
-
测试场景:搜索股票
- 完成基于 PO 模式封装的测试框架的搭建与优化。
实战思路
使用 PO 模式封装测试框架
PO 模式六大原则
- 属性意义
- 不要暴露页面内部的元素给外部
- 不需要建模 UI 内的所有元素
- 方法意义
- 用公共方法代表 UI 所提供的功能
- 方法应该返回其他的 PageObject 或者返回用于断言的数据
- 同样的行为不同的结果可以建模为不同的方法
- 不要在方法内加断言
构造页面相关类和方法
- 基础层:对底层工具进行二次封装,例如元素查找方法。
- 公共业务层:app 的启动配置。
- 页面层:每个页面或功能模块作为一个类,类中包含该页面的元素和操作方法。
- 测试用例层:使用 pytest 编写测试用例,调用业务逻辑层的方法进行测试。
- 公共方法层:一些通用的工具函数,例如日志记录、数据读取等。
填充测试框架
app 启动
# base/xueqiu_app.py
# 雪球 app 相关的操作
class XueqiuApp(BasePage):
def start(self):
'''
启动 app
:return:
'''
# 设置 cpability
caps = {
# 设置 app 安装的平台(Android,iOS)
"platformName": "Android",
# 设置 appium 驱动
"appium:automationName": "uiautomator2",
# 设置设备名称
"appium:deviceName": "emulator-5554",
# 设置被测 app 的包名
"appium:appPackage": "com.xueqiu.android",
# 设置被测 app 启动页面的 Activity
"appium:appActivity": ".view.WelcomeActivityAlias",
# 不清空缓存信息,保存登录信息
"appium:noReset": True,
# 强制app重启,整个测试运行之前重启 app
"appium:forceAppLaunch": True,
# 跳过安装,权限设置等操作
"appium:skipDeviceInitialization": True
}
# 初始化 driver
self.driver = webdriver.Remote(
"http://127.0.0.1:4723",
options=UiAutomator2Options().load_capabilities(caps)
)
self.driver.implicitly_wait(15)
return self
def stop(self):
'''
停止 app
:return:
'''
self.driver.quit()
def goto_main(self):
'''
进入 app 首页
:return:
'''
from auto_test_app.xueqiu_app_po.page.main_page import MainPage
return MainPage(self.driver)
BasePage 封装
# base/base_page.py
class BasePage:
def __init__(self, driver: WebDriver=None):
self.driver = driver
def find_ele(self, by, value):
'''
查找元素,返回元素
:param by: 定位方式
:param value: 元素定位表达式
:return: 定位到的元素对象
'''
step_text = f"查找单个元素的定位:{by},{value}"
logger.info(step_text)
ele = self.driver.find_element(by, value)
return ele
def find_eles(self, by, value):
'''
查找多个元素
:param by: 定位方式
:param value: 元素定位表达式
:return: 定位到的元素列表
'''
logger.info(f"查找多个元素的定位:{by},{value}")
eles = self.driver.find_elements(by, value)
return eles
业务页面封装
首页封装
# pages/main_page.py
class MainPage(XueqiuApp):
# 搜索框
_SEARCH_BAR = AppiumBy.XPATH, "//*[@resource-id='com.xueqiu.android:id/tv_banner']"
def goto_search_page(self):
# 点击首页搜索框
self.find_ele(*self._SEARCH_BAR).click()
# 跳转到搜索页面
return SearchPage(self.driver)
搜索页面封装
# pages/search_page.py
class SearchPage(XueqiuApp):
# 搜索框
_SEARCH_BAR = AppiumBy.ID,"com.xueqiu.android:id/search_input_text"
# 第一个搜索结果
_FIRST_SEARCH_RESULT = AppiumBy.XPATH, "//*[@resource-id='com.xueqiu.android:id/name']"
def goto_search_result_page(self, search_text):
# 搜索页面搜索框中输入搜索关键词
self.find_ele(*self._SEARCH_BAR).send_keys(search_text)
# 点击第一个搜索结果
self.find_eles(*self._FIRST_SEARCH_RESULT)[0].click()
# 跳转到搜索结果页面
return SearchResultPage(self.driver)
搜索结果页面封装
# pages/search_result_page.py
class SearchResultPage(XueqiuApp):
def get_search_result(self, search_text):
# 获取页面中是否包含搜索关键词相关结果
results = self.find_eles(
AppiumBy.XPATH,
f"//*[@text='{search_text}']"
)
results_text = []
if results:
# 获取每个元素的文本放入列表中
results_text = [r.text for r in results]
return results_text
编写测试用例
# testcases/test_search.py
class TestSearch:
def setup_method(self):
# 启动 app
self.app = XueqiuApp()
# 进入首页
self.main = self.app.start().goto_main()
def teardown_method(self):
# 关闭 driver
self.app.stop()
def test_search_stock(self):
'''
1 首页点击搜索框
2 搜索页点击搜索框,输入搜索关键词
3 点击第一个搜索结果
4 断言可以找到搜索关键词相关结果
:return:
'''
search_text = "阿里巴巴"
result = self.main.goto_search_page().\
goto_search_result_page(search_text).\
get_search_result(search_text)
assert search_text in result
优化测试框架
数据驱动
准备测试数据。
# datas/stock_name.yaml
- 阿里巴巴
- 平安银行
定义读取 yaml 文件的工具方法。
class Utils:
@classmethod
def get_yaml_data(cls, yaml_path):
'''
读取 yaml 文件的数据
:param yaml_path: yaml 文件的路径
:return:
'''
with open(yaml_path, "r", encoding="utf-8") as f:
datas = yaml.safe_load(f)
logger.info(f"读取的 yaml 文件的数据为 {datas}")
return datas
测试用例中读取 yaml 文件,完成数据驱动。
class TestSearch:
def setup_method(self):
# 获取雪球 app 的实例
self.app = XueqiuApp()
# 启动 app,进入首页
self.main = self.app.start().goto_main()
def teardown_method(self):
# 关闭 driver
self.app.stop()
@pytest.mark.parametrize(
"search_text",
Utils.get_yaml_data(Utils.get_file_path("./datas/stock_name.yaml"))
)
def test_search_stock(self, search_text):
'''
1. 首页点击搜索框
2. 搜索页点击搜索框,输入搜索关键词
3. 点击第一个搜索结果
4. 断言可以找到搜索关键词相关结果
:return:
'''
result = self.main.goto_search_page().\
input_search_key(search_text).\
get_search_result(search_text)
assert search_text in result
黑名单处理
# 定义弹窗黑名单
black_list = [
(AppiumBy.XPATH, "//*[@text=['取消']"),
(AppiumBy.XPATH, "//*[@text=['关闭']")
]
# 黑名单处理装饰器
# 传入 fun 相当于要去装饰的方式 find_ele(self, by, value)
def black_wrapper(fun):
def run(*args, **kwargs):
# 相当于拿到了传入参数的第一个值:self(BasePage 类的实例)
basepage = args[0]
basepage.driver.implicitly_wait(1)
try:
logger.info(f"开始查找元素 {args[1]}, {args[2]}")
basepage.driver.implicitly_wait(15)
return fun(*args, **kwargs)
except Exception as e:
logger.info(f"没有找到元素,处理异常 {e}")
# 截图
image_path = basepage.screenshot()
# 添加到 allure 报告
allure.attach.file(
image_path,
name="查找元素异常截图",
attachment_type=allure.attachment_type.PNG
)
# 保存页面源码
pagesource_path = basepage.save_pagesource()
allure.attach.file(
pagesource_path,
name="查找元素异常页面源码",
attachment_type=allure.attachment_type.TEXT
)
# 处理黑名单
for b in black_list:
# 查找黑名单列表中的每一个元素
eles = basepage.driver.find_elements(*b)
if len(eles) > 0:
# 找到了黑名单中的元素
basepage.driver.find_elements(*b)[0].click()
basepage.driver.implicitly_wait(15)
return fun(*args, **kwargs)
logger.info(f"遍历黑名单,仍未找到元素")
basepage.driver.implicitly_wait(15)
raise e
return run
在基类中使用定义好的黑名单装饰器装饰查找元素的方法。
# base/base_page.py
class BasePage:
def __init__(self, driver: WebDriver=None):
self.driver = driver
@black_wrapper
def find_ele(self, by, value):
'''
查找元素,返回元素
:param by: 定位方式
:param value: 定位表达式
:return: 找到的元素
'''
info = f"查找元素,定位方式为 {by}, 定位表达式为 {value}"
logger.info(info)
with allure.step(info):
ele = self.driver.find_element(by, value)
return ele
@black_wrapper
def find_eles(self, by, value):
'''
查找多个元素
:param by: 定位方式
:param value: 定位表达式
:return: 找到的元素列表
'''
info = f"查找多个元素,定位方式为 {by}, 定位表达式为 {value}"
logger.info(info)
with allure.step(info):
eles = self.driver.find_elements(by, value)
return eles
测试报告
添加描述信息
@allure.feature("雪球搜索")
class TestSearch:
def setup_method(self):
# 获取雪球 app 的实例
self.app = XueqiuApp()
# 启动 app,进入首页
self.main = self.app.start().goto_main()
def teardown_method(self):
# 关闭 driver
self.app.stop()
@allure.story("雪球搜索股票")
@allure.title("参数化搜索股票名称 {search_text}")
@pytest.mark.parametrize(
"search_text",
Utils.get_yaml_data(Utils.get_file_path("./datas/stock_name.yaml"))
)
def test_search_stock(self, search_text):
'''
1. 首页点击搜索框
2. 搜索页点击搜索框,输入搜索关键词
3. 点击第一个搜索结果
4. 断言可以找到搜索关键词相关结果
:return:
'''
result = self.main.goto_search_page().\
input_search_key(search_text).\
get_search_result(search_text)
assert search_text in result
生成测试报告
# 运行测试用例,搜集执行结果
pytest --alluredir=./results --clean-alluredir
# 启动本地服务查看 allure 报告
allure serve ./results
# 生成静态报告
allure generate --clean report/html report -o report/html
总结
- 使用 PageObject 模式封装 app 自动化测试框架
- 优化测试框架
- 测试数据的数据驱动
- 异常处理(弹窗黑名单),异常截图
- 日志记录
- 报告生成