PO模式是国外大神Martin Fowler于2013年提出来的一种设计模式,其基本**是强调代码逻辑和业务逻辑相分离。
https://martinfowler.com/bliki/PageObject.html
公共方法表示页面提供的服务 尽量不要暴露页面的内部实现 页面中不要加断言,断言加载 方法返回另外的页面对象 不需要封装全部的页面元素 相同的行为、不同的结果,需要封装成不同的方法
- 用Page Object表示UI
- 减少重复样本代码
- 让变更范围控制在Page Object内
- 本质是面向对象编程
- Driver对象:完成对WEB、Android、iOS、接口的驱动
- Page对象:完成对页面的封装
- 测试用例:调用Page对象实现业务并断言
- 数据封装:配置文件和数据驱动
- Utils:其他功能/工具封装,改善原生框架不足
- Driver层完成对webdriver常用方法的二次封装,如:定位元素方法;
- Elements层:存放元素属性值,如图标、按钮的resourceId、className等;
- Page层:存放页面对象,通常一个UI界面封装一个对象类;
- Case层:调用各个页面对象类,组合业务逻辑、形成测试用例。
四层模型与三层模型唯一的区别就是将Page层与Elements层存放在一起; 各个页面对象文件同时包含当前页面中各个图标、按钮的resourceId、className等属性值,以便随时调用。
.
├── conftest.py
├── pom
│ ├── __init__.py
│ ├── basepage.py
│ ├── bbspage
│ │ ├── __init__.py
│ │ ├── searchpage.py
│ └── mainpage
│ ├── __init__.py
│ ├── bbspage.py
│ ├── homepage.py
│ └── minepage.py
├── resource
│ └── app-auto-test
│ ├── zeekr-01-01.png
│ ├── zeekr-01-02.png
│ ├── zeekr-01-03.png
│ ├── zeekr-02-01.png
│ ├── zeekr-02-02.png
│ ├── zeekr-02-03.png
│ ├── zeekr-03-01.png
│ ├── zeekr-03-02.png
│ ├── zeekr-03-03.png
│ ├── zeekr-04-01.png
│ ├── zeekr-04-02.png
│ ├── zeekr-04-03.png
│ ├── zeekr-05-01.png
│ ├── zeekr-05-02.png
│ └── zeekr-05-03.png
└── testcases
├── __init__.py
└── test_hot_search.py
# _*_coding: utf-8 _*_
"""
Create by Zimo on 2022/3/10.
"""
import time
import pytest
from appium import webdriver # appium 的 webdriver
@pytest.fixture(scope='session')
def driver():
desired_caps = {
"platformName": "Android",
"appium:platformVersion": "11",
"appium:automationName": "UiAutomator2",
"appium:deviceName": "HUAWEI",
"appium:appPackage": "com.zeekrlife.mobile",
"appium:appActivity": "com.zeekrlife.main.SplashActivity",
"appium:noReset": True,
"appium:autoAcceptAlerts": True
}
app = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps)
time.sleep(3)
app.implicitly_wait(10) # 全局的隐式等待时间
yield app # 将driver传递出来
# 所有的用例执行之后
app.quit()
即Driver层,对uiautomator2进行二次封装,所有Page类都会直接或间接继承BasePage
# _*_coding: utf-8 _*_
"""
所有页面的基类
Create by Zimo on 2022/3/12.
"""
from appium.webdriver.webdriver import WebDriver
from appium.webdriver.common.appiumby import AppiumBy as By
command_ime_list = 'adb shell ime list -s'
command_set_sogou = 'adb shell ime set com.sohu.inputmethod.sogou/.SogouIME'
command_set_unicode = 'adb shell ime set io.appium.settings/.UnicodeIME'
class BasePage:
"""
第一层:对WebDriver进行二次封装,定义一个所有页面都继承的BasePage
封装WebDriver基本方法,如:元素定位,元素等待,翻页等
不需要全部封装,用到多少就封装多少
"""
def __init__(self, webdriver: WebDriver):
self.app = webdriver
self.size = self.app.get_window_size()
def find_element_by_id(self, element_id):
"""通过id定位单个元素"""
try:
return self.app.find_element(By.ID, element_id)
except Exception as e:
print(f'页面中没有找到id为{element_id}的元素')
raise e
def find_elements_by_id(self, element_id):
"""通过id定位多个元素"""
try:
return self.app.find_elements(By.ID, element_id)
except Exception as e:
print(f'页面中没有找到id为{element_id}的元素')
raise e
def find_element_by_xpath(self, element_xpath):
"""通过xpath定位单个元素"""
try:
return self.app.find_element(By.XPATH, element_xpath)
except Exception as e:
print(f'页面中没有找到xpath为{element_xpath}的元素')
raise e
def find_elements_by_xpath(self, element_xpath):
"""通过xpath定位多个元素"""
try:
return self.app.find_elements(By.XPATH, element_xpath)
except Exception as e:
print(f'页面中没有找到xpath为{element_xpath}的元素')
raise e
def get_toast_text(self) -> str:
"""获取toast文本"""
toast = self.app.find_element(By.XPATH, '//android.widget.Toast')
return toast.text
def swipe_up(self, start_ratio: float, ratio=0.5, x=0.5, t=2000, n=1):
"""向上滑动屏幕"""
if 0 < start_ratio - ratio < 1:
start_x = int(self.size['width'] * x)
start_y = int(self.size['height'] * start_ratio)
end_y = int(self.size['height'] * (start_ratio - ratio))
for i in range(n):
self.app.swipe(start_x, start_y, start_x, end_y, t)
def swipe_down(self, start_ratio: float, ratio=0.5, x=0.5, t=2000, n=1):
"""向下滑动屏幕"""
if 0 < ratio + start_ratio < 1:
start_x = int(self.size['width'] * x)
start_y = int(self.size['height'] * start_ratio)
end_y = int(self.size['height'] * (start_ratio, + ratio))
for i in range(n):
self.app.swipe(start_x, start_y, start_x, end_y, t)
def swipe_left(self, start_ratio: float, ratio=0.5, y=0.5, t=2000, n=1):
"""向左滑动屏幕"""
if 0 < start_ratio - ratio < 1:
start_x = int(self.size['width'] * start_ratio)
start_y = int(self.size['height'] * y)
end_x = int(self.size['width'] * (start_ratio - ratio))
for i in range(n):
self.app.swipe(start_x, start_y, end_x, start_y, t)
def swipe_right(self, start_ratio: float, ratio=0.5, y=0.5, t=2000, n=1):
"""向右滑动屏幕"""
if 0 < ratio + start_ratio < 1:
start_x = int(self.size['width'] * start_ratio)
start_y = int(self.size['height'] * y)
end_x = int(self.size['width'] * (start_ratio + ratio))
for i in range(n):
self.app.swipe(start_x, start_y, end_x, start_y, t)
def swipe_custom(self, start_x, start_y, end_x, end_y, t=500):
"""自定义滑动屏幕"""
self.app.swipe(start_x, start_y, end_x, end_y, t)
所有页面Page类都继承BasePage。根据PO模式六大原则之一的“不需要封装全部的页面元素”,用到多少页面元素就封装多少。例如:当前待测APP有3个界面,则定义3个页面Page:
- home_page.py
- bbs_page.py
- mine_page.py
# _*_coding: utf-8 _*_
"""
Create by Zimo on 2022/3/12.
"""
from appium.webdriver.webdriver import WebDriver
from pom.basepage import BasePage
from pom.mainpage.bbspage import BbsPage
from pom.mainpage.findpage import FindPage
from pom.mainpage.minepage import MinePage
class HomePage(BasePage):
"""
主App模型
"""
def __init__(self, webdriver: WebDriver):
super().__init__(webdriver)
self.bbs_icon = 'com.zeekrlife.mobile:id/tab_community'
self.find_icon = 'com.zeekrlife.mobile:id/tab_message'
self.vehicle_icon = 'com.zeekrlife.mobile:id/tab_vehicle'
self.mall_icon = 'com.zeekrlife.mobile:id/tab_mall'
self.mine_icon = 'com.zeekrlife.mobile:id/tab_mine'
def click_bbs_icon(self) -> BbsPage:
"""
跳转"社区"页面
:return: BbsPage
"""
self.find_element_by_id(self.bbs_icon).click()
return BbsPage(self.app)
def click_find_icon(self):
"""
跳转"发现"页面
:return: FindPage
"""
self.find_element_by_id(self.find_icon).click()
return FindPage(self.app)
def click_vehicle_icon(self):
"""
跳转"爱车"页面
:return:
"""
self.find_element_by_id(self.vehicle_icon).click()
# TODO
def click_mall_icon(self):
"""
跳转"极物"页面
:return:
"""
self.find_element_by_id(self.mall_icon).click()
# TODO
def click_mine_icon(self):
"""
跳转"我的"页面
:return: MinePage
"""
self.find_element_by_id(self.mine_icon).click()
return MinePage(self.app)
# _*_coding: utf-8 _*_
"""
Create by Zimo on 2022/3/12.
"""
from appium.webdriver.webdriver import WebDriver
from pom.basepage import BasePage
from pom.bbspage.writeshortpostpage import WriteShortPostPage
class BbsPage(BasePage):
"""
"社区"页面模型
"""
def __init__(self, webdriver: WebDriver):
super().__init__(webdriver)
self.add_icon = 'com.zeekrlife.mobile:id/imgv_add'
self.short_icon = 'com.zeekrlife.mobile:id/tv_short'
self.long_icon = 'com.zeekrlife.mobile:id/tv_long'
self.question_icon = 'com.zeekrlife.mobile:id/tv_question'
self.friend_icon = 'com.zeekrlife.mobile:id/new_friend'
self.scan_icon = 'com.zeekrlife.mobile:id/tv_scan'
self.search_box = 'com.zeekrlife.mobile:id/search_view_flipper'
self.recommend_tab = '//android.widget.LinearLayout[@content-desc="推荐"]'
self.news_tab = '//android.widget.LinearLayout[@content-desc="资讯"]'
self.faq_tab = '//android.widget.LinearLayout[@content-desc="问答"]'
self.dynamic_tab = '//android.widget.LinearLayout[@content-desc="动态"]'
def click_add(self):
"""
点击"+"号
:return: None
"""
self.find_element_by_id(self.add_icon).click()
# time.sleep(1)
def click_short_icon(self):
"""
发动态
:return: WriteShortPostPage
"""
self.find_element_by_id(self.short_icon).click()
return WriteShortPostPage(self.app)
def click_long_icon(self):
"""
发文章
:return:
"""
self.find_element_by_id(self.long_icon).click()
# TODO
def click_question_icon(self):
"""
去提问
:return:
"""
self.click_add()
self.find_element_by_id(self.question_icon).click()
# TODO
def click_friend_icon(self):
"""
加好友
:return:
"""
self.click_add()
self.find_element_by_id(self.friend_icon).click()
# TODO
def click_scan_icon(self):
"""
扫一扫
:return:
"""
self.click_add()
self.find_element_by_id(self.scan_icon).click()
# TODO
def click_search_box(self):
"""
跳转"搜索"页面
:return:
"""
self.find_element_by_id(self.search_box).click()
# TODO
def click_recommend_tab(self):
"""
跳转"推荐"页面
:return:
"""
self.find_element_by_xpath(self.recommend_tab).click()
# TODO
def click_news_tab(self):
"""
跳转"资讯"页面
:return:
"""
self.find_element_by_xpath(self.news_tab).click()
# TODO
def click_faq_tab(self):
"""
跳转"问答"页面
:return:
"""
self.find_element_by_xpath(self.faq_tab).click()
# TODO
def click_dynamic_tab(self):
"""
跳转"动态"页面
:return:
"""
self.find_element_by_xpath(self.dynamic_tab).click()
# TODO
# _*_coding: utf-8 _*_
"""
Create by Zimo on 2022/3/13.
"""
from appium.webdriver.webdriver import WebDriver
from pom.basepage import BasePage
class SearchPage(BasePage):
"""
搜索-页面模型
"""
def __init__(self, webdriver: WebDriver):
super().__init__(webdriver)
self.search_box = 'com.zeekrlife.mobile:id/editSearch'
self.synthesize_tab = '//android.widget.LinearLayout[@content-desc="内容"]'
self.faq_tab = '//android.widget.LinearLayout[@content-desc="问答"]'
self.square_tab = '//android.widget.LinearLayout[@content-desc="广场"]'
self.activity_tab = '//android.widget.LinearLayout[@content-desc="活动"]'
self.user_tab = '//android.widget.LinearLayout[@content-desc="用户"]'
self.clear_search_icon = 'com.zeekrlife.mobile:id/imgv_clear'
self.delete_history_icon = 'com.zeekrlife.mobile:id/imgv_delete'
self.search_history = '//*[@resource-id="com.zeekrlife.mobile:id/flexbox"]/android.widget.TextView'
self.back_icon = 'com.zeekrlife.mobile:id/toolbar_left_imgv'
def input_search_box(self, content):
"""
搜索
:param content:搜索内容
:return:None
"""
self.find_element_by_id(self.search_box).click()
self.find_element_by_id(self.search_box).send_keys(content)
self.app.keyevent(66)
def click_synthesize_tab(self):
"""
点击综合
:return:None
"""
self.find_element_by_xpath(self.synthesize_tab).click()
def click_faq_tab(self):
"""
点击问答
:return:None
"""
self.find_element_by_xpath().click()
def click_square_tab(self):
"""
点击广场
:return:None
"""
self.find_element_by_xpath(self.square_tab).click()
def click_activity_tab(self):
"""
点击活动
:return:None
"""
self.find_element_by_xpath(self.activity_tab).click()
def click_user_tab(self):
"""
点击用户
:return:None
"""
self.find_element_by_xpath(self.user_tab).click()
def clear_search_box(self):
"""
清除搜索内容
:return:None
"""
self.find_element_by_id(self.clear_search_icon).click()
def clear_search_history(self):
"""
删除历史搜索记录
:return:None
"""
self.find_element_by_id(self.delete_history_icon).click()
def get_search_history(self):
"""
获取搜索历史
:return:None
"""
search_history_list = []
ele_list = self.find_elements_by_xpath(self.search_history)
for search_history in ele_list:
search_history_list.append(search_history.text)
return search_history_list
def click_back_icon(self):
"""
返回社区页面
:return:None
"""
self.find_element_by_id(self.back_icon).click()
测试用例实际上是调用各个页面对象组合成的一个业务逻辑集合,中间再加入一些控制结构(选择结构if...else、循环结构for)、断言等,就形成了最终的测试用例。
# _*_coding: utf-8 _*_
"""
环境测试
Create by Zimo on 2022/3/10.
"""
from pom.mainpage.homepage import HomePage
def test_search(driver):
home = HomePage(driver)
bbs = home.click_bbs_icon()
search = bbs.click_search_box()
search.search("续航")
search.clear_search_box()
search_history = search.get_search_history()
assert "续航" == search_history[0]