前言:
Page Object是UI自动化测试项目开发实践的最佳设计模式之一,它的主要特点体现在对界面交互细节的封装上,是测试用例更加专注于业务的操作,从而提高测试用例的可维护性。
1 认识 Page Object
当为web页面编写测试时,需要操作该页面上的元素。然而,如果在测试代码中直接操作web页面上的元素,那么这样的代码是极其脆弱,不好维护的,因为UI的变化是非常频繁的。
page对象的一个基本经验法则是:凡是人能做的事,page对象通过软件客户端都能做到。因此,它提供了一个易于编程的接口,并隐藏窗口中底层的部件。当访问一个文本框时,应该通过一个访问方法实现字符串的获取与返回,复选框应当使用布尔值,按钮应当被表示为行为导向的方法名。page对象应当把在GUI空间上所有查询和操作数据的行为封装为方法。
一个好的经验法则是,即使改变具体的元素,page对象的接口也不应当发生变化。
尽管术语是page对象,但并不意味着需要针对每个页面建立一个这样的对象。例如,页面上有重要意义的元素可以独立为一个page对象。经验法则的目的是通过给页面建模,使其对应用程序的使用者变得有意义。
Page Object是一种设计模式,在自动化测试开发中应该遵循这种设计模式来编写代码:
①:Page Object 应该易于使用。
②:有清晰的结构,如PageObjects对应页面对象,PageModules对应页面内容。
③:只写测试内容,不写基础内容。
④:在可能的情况下放置样板代码。
⑤:不需要自己管理浏览器。
⑥:在运行时选择浏览器,而不是类级别。
⑦:不需要直接接触selenium
2 实现Page Object
2.1 Page Object 的简单实例
以百度搜索为例,假设我们有如下测试代码,baidu_search.py
class BaiDuTest():
def __init__(self, driver):
self.driver = driver
self.base_url = "http://baidu.com"
def test_baidu_search_case1(self):
self.driver.get(self.base_url)
self.driver.find_element_by_id("kw").send_keys("selenium")
self.driver.find_element_by_id("su").click()
def test_baidu_search_case2(self):
self.driver.get(self.base_url)
self.driver.find_element_by_id("kw").send_keys("unittest")
self.driver.find_element_by_id("su").click()
def test_baidu_search_case3(self):
self.driver.get(self.base_url)
self.driver.find_element_by_id("kw").send_keys("page object")
self.driver.find_element_by_id("su").click()
这段代码的最大问题就是在三条测试用例中重复使用了元素的定位和操作。这会带来一个很大的问题。当元素的定位发生变化后,如kw失效,应该及时调整定位方法,这时就需要在三条测试用例中修改,如果我们有几百条测试用例,而UI的变化又很频繁,那么就会提高自动化测试用例的维护成本。
Page Object的设计思想是把元素定位和元素操作进行封层,这样带来的最直接的好处就是当元素发生变化时,只需维护page层的元素定位,而不需要关系在那些测试用例当中使用了这些元素。在编写测试用例时,也不需要关心元素是如何定位的。
比如:baidu_page.py
class BaiduPage():
def __init__(self, driver):
self.driver = driver
def search_input(self, search_key):
self.driver.find_element_by_id("kw").send_keys(search_key)
def search_button(self):
self.driver.find_element_by_id("su").click()
这里只针对一个页面中可能会操作的元素进行了封装,原则上是一个元素封装成一个方法,当元素的定位方法发生改变时,只需要维护这个方法即可,而不需要关心这个方法被那些测试用例使用过!!
那么刚开始提到的baidu_search.py可以进行如下修改
from .baidu_page import BaiduPage
class BaiDuTest():
def __init__(self, driver):
self.driver = driver
self.base_url = "http://baidu.com"
def test_baidu_search_case1(self):
self.driver.get(self.base_url)
bd = BaiduPage(self.driver)
bd.search_input("selenium")
bd.search_button()
def test_baidu_search_case2(self):
self.driver.get(self.base_url)
bd = BaiduPage(self.driver)
bd.search_input("unittest")
bd.search_button()
def test_baidu_search_case3(self):
self.driver.get(self.base_url)
bd = BaiduPage(self.driver)
bd.search_input("page object")
bd.search_button()
如上就可以轻松的使用它封装的方法来设计具体的测试用例了,这样做的目的就是在测试用例中消除元素定位。
2.2 改进Page Object封装
上面的例子只是说明了Page Object设计模式的基本原理,这样分层确实带来了不少好处,但同时也带来了一些问题。比如,要写更多的代码,以前一条测试用例只需要几行,现在却不得不现在Page层针对每个待操作的元素进行封装,然后再到具体的用例中引用。为了使Page层的封装更加方便,做一下改进:
2.1.1创建base.py
创建BasePage类作为所有page类的基类,在其中封装一些常用的方法。(其他自己想封装的也可以!)
class BasePage():
def __init__(self, driver):
self.driver = driver
self.url = ""
# 打开页面
def open_page(self, url=None):
if url:
self.driver.get(url)
else:
self.driver.get(self.url)
# id定位
def by_id(self, id_):
return self.driver.find_element_by_id(id_)
# name定位
def by_name(self, name):
return self.driver.find_element_by_name(name)
# class定位
def by_class(self, class_name):
return self.driver.find_element_by_class_name(class_name)
# xpath定位
def by_xpath(self, xpath):
return self.driver.find_element_by_xpath(xpath)
# css定位
def by_css(self, css):
return self.driver.find_element_by_css_selector(css)
# 获取title
def get_title(self):
return self.driver.title
# 获取text,暂时仅以xpath方式为例
def get_text(self, xpath):
return self.by_xpath(xpath).text
# 执行js
def js(self, script):
self.driver.excute_script(script)
修改baidu_page.py
from .base import BasePage
class BaiPage(BasePage):
url = "http://www.baidu.com"
def search_input(self, search_key):
self.by_id("kw").send_keys(search_key)
def search_button(self):
self.by_id("su").click()
下面我们使用上面封装好的base类来简单实现一个测试用例
from pageObject.baidu_page1 import BaiDuPage
import unittest
from time import sleep
from selenium import webdriver
chrome_driver_path = r"C:\Users\Administrator\Envs\selenuimAutoTest\Lib\site-packages\selenium\webdriver\chrome\chromedriver.exe"
# driver = webdriver.Chrome(executable_path=chrome_driver_path)
class BaiduTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome(executable_path=chrome_driver_path)
def test_baidu_search_case(self):
page = BaiDuPage(self.driver)
page.open_page()
page.search_input("selenium")
page.search_button()
sleep(2)
self.assertEqual(page.get_title(), "selenium_百度搜索")
@classmethod
def tearDownClass(cls):
cls.driver.quit()
if __name__ == '__main__':
unittest.main()
我们在编写测试用例时,是不是方便了不少,当需要Page类时,只需要将浏览器驱动传入就可以了,其中的方法调用起来也是很方便
3 poium测试库
poium是一个基于selenium/appium的Page Object测试库,最大特点是简化了Page层元素的定义。需要pip安装
3.1 基本使用
修改 baidu_page.py
from poium import Element, Page
class BaiduPage(Page):
search_input = Element(id_="kw") # 定位搜索框
search_button = Element(id_="su") # 定位搜索按钮
修改baidu_search.py
from pageObject.baidu_page2 import BaiduPage
import unittest
from time import sleep
from selenium import webdriver
chrome_driver_path = r"C:\Users\Administrator\Envs\selenuimAutoTest\Lib\site-packages\selenium\webdriver\chrome\chromedriver.exe"
# driver = webdriver.Chrome(executable_path=chrome_driver_path)
class BaiduTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome(executable_path=chrome_driver_path)
def test_baidu_search_case(self):
page = BaiduPage(self.driver)
page.get("http://www.baidu.com") # 请求url
page.search_input.send_keys("selenium") # 搜索框输入
page.search_button.click() # 点击搜索框
sleep(2)
self.assertEqual(page.get_title, "selenium_百度搜索")
@classmethod
def tearDownClass(cls):
cls.driver.quit()
这个例子的page层是不是比上面2.2的page封装层又简单了很多,只需要继承提供的Page类,Element类即可
3.2 更多用法
3.2.1 poium支持的定位方式有:
源码:
LOCATOR_LIST = {
# selenium
'css': By.CSS_SELECTOR,
'id_': By.ID,
'name': By.NAME,
'xpath': By.XPATH,
'link_text': By.LINK_TEXT,
'partial_link_text': By.PARTIAL_LINK_TEXT,
'tag': By.TAG_NAME,
'class_name': By.CLASS_NAME,
# appium
'ios_uiautomation': MobileBy.IOS_UIAUTOMATION,
'ios_predicate': MobileBy.IOS_PREDICATE,
'ios_class_chain': MobileBy.IOS_CLASS_CHAIN,
'android_uiautomator': MobileBy.ANDROID_UIAUTOMATOR,
'android_viewtag': MobileBy.ANDROID_VIEWTAG,
'android_data_matcher': MobileBy.ANDROID_DATA_MATCHER,
'android_view_matcher': MobileBy.ANDROID_VIEW_MATCHER,
'windows_uiautomation': MobileBy.WINDOWS_UI_AUTOMATION,
'accessibility_id': MobileBy.ACCESSIBILITY_ID,
'image': MobileBy.IMAGE,
'custom': MobileBy.CUSTOM,
}
具体用法如下:
Element(id_="id值")
Element(name="name值")
Element(class_name="class值")
Element(tag="tag名")
Element(link_text="超链接文本")
Element(partial_link_text="部分超链接文本")
Element(xpath="xpath表达式")
Element(css="如:#id")
3.2.2 设置超时时间
通过参数timeout设置超时时间
Element(id_="id值", timeout=5)
3.2.3 设置元素描述
当一个page类中定义的元素非常多时,有必要使用注释来增加可读性,这时可以使用describe参数,其实该参数并无实际意义,只是增加了可读性
Element(id_="id值", timeout=5,describe="搜索框")
3.2.4 定为一组元素
当我们要定位一组元素时,可以使用其另外一个类,Elements
Elements(xpath="xpath表达式")
poium极大的简化了Page层的定义,除此之外,它还提供了很多的API,如PageSelect类简化了下拉框的处理等,具体可以到文档中了解。https://pypi.org/project/poium/