Appium 自动化测试 PageObject 封装
PageObject 封装
应用背景
传统编写测试用例方式,使用单元测试框架(unittest,pytest等)维护测试用例,将业务测试详细操作步骤都编写在测试用例中。这种方式无法适应 UI 变化,一旦 UI 界面元素变化会导致大量的 case 需要修改。而且用例中存在大量的样板代码,比如driver,find,click等,不但无法清晰表达用例的业务场景,维护起来也相当耗费人力和时间成本。
Page Object模式作为最为公认的一种设计模式,在设计UI自动化测试用例时,把元素和方法按照页面抽象出来,实现页面对象和测试用例的分离。使用 PO 模式维护测试用例可以减少代码的重复率,提高代码的可读性以及可维护性。
PageObject 简介
在为 UI 页面写测试用例时(比如 Web 页面,移动端页面),测试用例会存在大量元素和操作细节。如何面对当 UI 变化时,测试用例也要跟着变化这个问题?PageObject 设计模式闪亮登场(由 IT 大佬 Martin Flower 提出)。
使用 UI 自动化测试工具时(Selenium、Appium 等),如果无统一模式进行规范,随着用例的增多会变得难以维护,而 PageObject 让自动化脚本井井有序,将 page 单独维护并封装细节,可以使 testcase 更稳健,不需要大改动。
PageObject 使用
具体做法:把元素信息和操作细节封装到 Page 类中,在测试用例上调用 Page 对象(PageObject),比如存在一个功能“选取相册标题”,需要为之建立函数selectAblumWithTitle(),函数内部是操作细节findElementsWithClass('album')等:
以“选取相册标题”举例,伪代码如下:
selectAblumWithTitle() {
#选取相册
findElementsWithClass('album')
#选取相册标题
findElementsWithClass('title-field')
#返回标题内容
return getText()
} PageObject 的主要原则是提供一个简单接口 (或者函数,比如上述的 selectAblumWithTitle ),让调用者在页面上可以做任何操作,点击页面元素,在输入框输入内容等。因此,如果要访问一个文本字段,Page Object 应该有获取和返回字符串的方法。Page Object 应该封装对数据的操作细节,比如查找元素和点击元素。当页面元素改动时,应该只改变 Page 类中的内容,不需要改变调用它的地方。
不要为每个 UI 页面都创建一个 page 类,应该只为页面中重要的元素创建 page 类。比如,一个页面显示多个相册,应该创建一个相册列表 page object,它包含许多相册 page object。如果某些复杂 UI 的层次结构只是用来组织 UI,那么它就不应该出现在 page object 中。page object 的目的是通过给页面建模,从而对应用程序的使用者变得有意义:
如果你想导航到另一个页面,初始 page 对象应当 return 另一个 page 对象,比如点击注册,进入注册页面,在代码中就应该 return Register()。如果想获取页面信息,可以 return 基本类型(字符串、日期)。
建议不要在 page object 中放断言。应该去测 page object,而不是让 page object 自己测自己,page object 的责任是提供页面的状态信息。这里仅用 HTML 描述 Page Object,这种模式还可以用来隐藏 Java swing UI 细节,它可用于所有 UI 框架。
PageObject 六大原则
Selenium 针对 PageObject 的核心思想凝聚出了六大原则,掌握六大原则精髓,才可以进行 PageObject 最佳实践演练:
- 公共方法代表页面提供的服务
- 不要暴露页面细节
- 不要把断言和操作细节混用
- 方法可以 return 到新打开的页面
- 不要把整页内容都放到 PO 中
- 相同的行为会产生不同的结果,可以封装不同结果
下面,对上述六大原则进行更详细的实操解释:
- 原则一:要封装页面中的功能(或者服务),比如点击页面中的元素,可以进入到新的页面,于是,可以为这个服务封装方法“进入新页面”。
- 原则二:封装细节,对外只提供方法名(或者接口)。
- 原则三:封装的操作细节中不要使用断言,把断言放到单独的模块中,比如 testcase。
- 原则四:点击一个按钮会开启新的页面,可以用 return 方法表示跳转,比如return MainPage()表示跳转到新的 PO:MainPage。
- 原则五:只为页面中重要的元素进行 PO 设计,舍弃不重要的内容。
- 原则六:一个动作可能产生不同结果,比如点击按钮后,可能点击成功,也可能点击失败,为两种结果封装两个方法,click_success和click_error。
示例
PageObjetct 的模块关系如下,所有的模块要继承 BasePage , App 实现启动,重启,停止等操作, Main 实现进入搜索页,进入股票页等操作:
base_page 模块是所有 page 类的父类,其中定义了公共方法,比如封装下面的 find 方法后,可以让子类调用 find :
from appium.webdriver.webdriver import WebDriver
class BasePage:
_driver: WebDriver
def __init__(self, driver: WebDriver = None):
self._driver = driver
def find(self, locator, value: str = None):
#如果传进来的是tuple,只需使用一个参数:locator
if isinstance(locator, tuple):
return self._driver.find_element(*locator)
else:
return self._driver.find_element(locator, value) App模块封装 app 的启动,重启,停止等方法,当 app 启动时会进入 main 页面,因此在下面的main方法要 return Main ,Main类的定义在后面会讲解:
from appium import webdriver
from test_appium.page.base_page import BasePage
from test_appium.page.main import Main
class App(BasePage):
#指定app的包名和activity名
_package = "com.xueqiu.android"
_activity = ".view.WelcomeActivityAlias"
def start(self):
#如果driver为空则初始化
if self._driver is None:
caps = {}
caps["platformName"] = "android"
caps["deviceName"] = "hogwarts"
caps["appPackage"] = self._package
caps["appActivity"] = self._activity
caps["noReset"] = True
#初始化driver
self._driver = webdriver.Remote(
"http://localhost:4723/wd/hub",
caps)
self._driver.implicitly_wait(5)
#如果driver不为空,则直接启动activity
else:
print(self._driver)
self._driver.start_activity(self._package, self._activity)
return self
def restart(self):
pass
def stop(self):
pass
def main(self) -> Main:
#当app启动时,跳转到(实例化)Main
return Main(self._driver) Main模块是首页的 PageObject ,其中的方法封装了首页的重要功能,比如下面代码中的 goto_search_page 封装了点击搜索并跳转到 Search 页:
from appium.webdriver.common.mobileby import MobileBy
from selenium.webdriver.common.by import By
from test_appium.page.base_page import BasePage
from test_appium.page.profile import Profile
from test_appium.page.search import Search
class Main(BasePage):
#点击搜索按钮后,进入搜索页
def goto_search_page(self):
self.find(MobileBy.ID, "tv_search").click()
#进入搜索页
return Search(self._driver)
def goto_stocks(self):
pass
def goto_trade(self):
pass
def goto_messages(self):
pass Search 模块可以搜索一支股票,还可以获取股票的价格,比如下图:
封装代码如下:
from appium.webdriver.common.mobileby import MobileBy
from selenium.webdriver.remote.webdriver import WebDriver
class Search:
_driver: WebDriver
def __init__(self, driver):
self._driver = driver
#输入要搜索的内容
def search(self, key: str):
self._driver.find_element(
MobileBy.ID,
"search_input_text"
).send_keys(key)
self._driver.find_element(
MobileBy.ID,
"name"
).click()
return self
#获取股票价格,用于判断
def get_price(self, key: str) -> float:
return float(self._driver.find_element(
MobileBy.ID,
"current_price"
).text) 最后对上述代码建立测试,新建测试模块 test_search :
import pytest
from test_appium.page.app import App
class TestSearch:
def setup(self):
self.main = App().start().main()
def test_search(self):
assert self.main.\
goto_search_page().\
search("alibaba").\
get_price("BABA") > 200 上面的案例将首页和搜索页分别封装成 Page 页,将用到的元素和方法封装起来,使用 return 实现了页面切换。后面写其它测试用例需要用到相同的元素和方法,就可以直接实例一个已有的 Page 对象去调用里面的方法,大大提高了代码的复用率。
小结
初步掌握了 PageObject 模式之后,下⼀节我们重点讲解如何就 Appium 问题进⾏分析定位。
<p> 专刊包含了10+年经验测试架构师对测试职业发展的深度解读 帮助你掌握当下 BAT 流行的 App 自动化测试技术基础技能和工具使用;以及从入门到进阶的自动化测试实战经验,在面试中能够脱颖而出。 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p> <p> <br /> </p>
曼迪匹艾公司福利 115人发布