Page Object是UI自动化测试项目开发实践的最佳实践之一,在接口交互的详细包中体现出主要特征,将测试用例集中于业务操作,提高测试用例的维护性。
一、认识Page Object
当为web页面编写测试时,需要操作网页上的元素。但是,如果在测试代码中直接操作web页面上的元素,那么这样的代码是极其脆弱的,因为UI会经常变化。
page对象的一个基本经验法则是,任何人都可以通过软件客户端完成page对象。因此,必须提供易于编程的界面,并隐藏窗口底部的部件。访问文件箱时,需要使用访问方法( Accessor Method )获取和返回字符串。 复选框必须使用布尔值,按钮必须以贝壳形式显示为指导方法名称。 page对象必须将GUI控件上的所有查询和操作数据的行为封装为方法。
一个好的经验法则是,即使改变具体的元素,page对象的接口也不应当发生变化。
虽然这个术语是page对象,但并不意味着每个页面都必须创建这样的对象。例如,页面上具有重要意义的元素可以独立为一个page对象。 经验法则的目的是通过对页面进行建模,使应用程序的使用者有意义。
Page Object是一种设计模式,在自动化测试开发中应遵循这种设计模式来编写代码。
Page Object应该遵循以下原则进行开发:
- Page Object应该易于使用
- 有清晰的结构,如PageObjects对应页面对象,PageModules对应页面内容
- 只写测试内容,不写基础内容
- 在可能的情况下防止样板代码
- 不需要自己管理浏览器
- 在运行时选择浏览器,而不是类级别
- 不需要直接接触Selenium
二、实现Page Object
下面我们将通过例子介绍这种设计模式的使用
2.1 Paget Object简单实例:
以百度搜索为例,假设我们有如下测试代码:
...
def test_baidu_search_casel(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_casel(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_casel(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()
...
这段代码最大的问题是元素的定位和操作在三个测试用例中被重用。这会带来一个大问题。当一个元件的定位发生变化时,例如id=kw失效,应及时调整定位方法。此时需要在三个测试用例中进行修改。假设我们的自动化项目中有上百个测试用例,UI很可能会频繁变化,这样会增加自动化测试用例的维护成本。
Page Object的设计思想最直接的优点是,通过将元素放置在元素操作中进行分层,在元素发生变化时,只需要保持page层的元素放置,而不需要关心在哪个测试用例中使用了这些元素。 在创建测试用例时,也不需要关心元素如何定位。
创建baidu_page.py文件,内容如下:
class BaiduPage():
# 初始化驱动器
def __init__(self, driver):
self.driver = driver
# 搜索输入框定位方法以及内容输入
def search_input(self, search_key):
# id定位
self.driver.find_element_by_id("wk").send_keys(search_key)
# 按钮的定位点击
def search_button(self):
self.driver.find_element_by_id("su").click()
首先创建BaiduPage类,在__init__()初始化方法中接收参数driver,分配给self.driver…然后,封装search_input()方法和search_button()方法,定位和操作元素。这里的封装只针对可以在页面上操作的元素。原则上,一个元素被封装到一个方法中。当一个元素的定位方法发生变化的时候,你只需要在这里维护方法,而不需要关心这个方法是在哪个测试用例中使用的。
from baidu_page import BaiduPage
...
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 pbject")
bd.search_button()
...
首先将BaiduPage类导入到测试中,然后将驱动引入到每个测试用例中,这样就很容易使用其封装的方法来设计具体的测试用例。这样做的目的是消除测试用例中的元素定位。想要操作百度输入框,只需要调用search_input()方法,传入搜索关键词,不需要关心百度输入框是怎么定位的。
2.2改进Paget Object封装
上例给出了Page Object设计模型的基本原理。这样的分层确实会带来不好的好处,但也存在一些问题。例如,需要写更多的代码。以前的测试用例只需要写4~5行代码,但是在特定的测试用例中引用之前,必须对Page层中操作的每个元素进行封装。为了便于封装Page层,进行了一些改进。
创建base.py文件,内容如下:
import time
class BasePage:
"""
基础Page层,封装一些常用方法
"""
def __init__(self, driver):
self.driver = driver
# 打开页面
def open(self, url=None):
if url is None:
self.driver.get(self.url)
else:
self.driver.get(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)
# 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
# 执行JavaScript脚本
def js(self, script):
self.driver.excuye_script(script)
创建BasePage类作为所有Page类的基类,在BasePage类中封装一些方法,这些方法是我们在做自动化时经常用到的。
open()方法用于打开网页,它接收一个url参数,默认为None。如果url参数为None,则默认打开子类中定义的url。稍后会在子类中定义url变量。
by_id()和by_name()方法,我们知道,Selenium提供的元素定位方法很长,这里做了简化,只是为了在子类中使用更加简便。
get_title()和get_text()方法。这些方法是在写自动化测试时经常用到的方法,也可以定义在BasePage类中。需要注意的是,get_text()方法需要接收元素定位,这里默认为XPath定位。
当然,我们还可以根据自己的需求封装更多的方法到BasePage类中。
修改baidu_page.py文件
from base import BasePage
class BaiduPage(BasePage):
"""
百度Page层,百度页面封装操作到的元素
"""
url = "https://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()
创建BaiduPage.py类继承BasePage类,定义url变量,供父类中的open()方法使用。这里可能会有点绕,所以举个例子:
小明的父亲有一辆电动玩具汽车,电动玩具汽车需要电池才能跑起来,但小明的父亲并没有为电动玩具汽车安装电池。小明继承了父亲的这辆电动玩具汽车,为了让电动玩具汽车跑起来,小明购买了电池。在这个例子中,open()方法就是"电动玩具汽车",open()方法中使用的self.url就是"电池",子类中定义的url是为了给父类中的open()方法使用的。
在search_input()和search_buttom()方法中使用了父类的self.by_id()方法来定位元素,比原始的Selenium方法简短了不少。
在测试用例中,使用BaiduPage类及它所继承的父类中的方法。
import unittest
from time import sleep
from selenium import webdriver
from baidu_page import BaiduPage
class TestBaidu(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome()
def test_baidu_search_case(self):
page = BaiduPage(self.driver)
page.open()
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(verbosity=2)
因为前面封装了元素的定位,所以在编写测试用例时会方便不少,当需要用到哪个Page类时,只需将它传入浏览器驱动,就可以使用该类中提供的方法了。
2.3poium测试库
poium是一个基于Selenuim/appium的Page Object测试库,最大的特点是简化了Page层元素的定义。
项目地址: https://github.com/defnngj/poium
支持pip安装:pip install poium
3.1基本使用
使用poium重写baidu_page.py
from poium import Page, PageElement
class BaiduPage(Page):
"""
百度Page层,百度页面封装操作的元素
"""
search_input = PageElement(id_="kw")
search_button = PageElement(id_="su")
创建BaiduPage类,使其继承poium库中的Page类。调用PageElement类定义元素定位,并赋值给变量search_input和search_button。这里仅封装元素的定位,并返回元素对象,元素的具体操作仍然在测试用例中完成,这也更加符合Page Object的思想,将元素定位与元素操作分层。
在测试用例中的使用如下。
from baidu_page import BaiduPage
class TestBaidu(unittest.TestCase):
...
def test_baidu_search_case1(self):
page = BaiduPage(self.driver)
page.get("https://www.baidu.com")
page.search_input = "selenium"
page.search_button.click()
...
首先导入BaiduPage类,传入浏览器驱动。然后,调用get()方法访问URL,该方法由Page类提供。接下来调用BaiduPage类中定义的元素对象,即search_input和search_button,实现相应的输入和单击操作。
更多用法
想要更好地使用poium,需要了解下面的一些使用技巧。
1、支持的定位方法
poium支持8种定位方式。
from poium import Page, PageElement
class SomePage(Page):
elem_id = PageElement(id_="id")
elem_name = PageElement(name="name")
elem_class = PageElement(class_name="class")
elem_tag = PageElement("tag"="input")
elem_link_text = PageElement(link_text="this_is_link")
elem_partial_link_text = PageElement(partial_link_text="is_link")
elem_xpath = PageElement(xpath='//*[@id="kk"]')
elem_css = PageElement(css="#id")
2、设置元素超时时间
通过timeout参数可设置元素超时时间,默认为10s
from poium import Page,PageElement
class BaiduPage(Page):
search_input = PageElement(id_="kw", timeout=5)
search_button = PageElement(id_="su", timeout=30)
3、设置元素描述
当一个Page类中定义的元素非常多时,必须通过注释来增加可读性,这时可以使用describe参数。
from poium import Page, PageElement
class LoginPage(Page):
"""
登录Page类
"""
username = PageElement(css="#loginAccount", describe="用户名")
password = PageElement(css="#loginPwd", describe="密码")
login_button = PageElement(css="#login_bin",describe="登录按钮")
user_info = PageElement(css="a.nev_user_name > span",describe="用户信息")
需要强调的是,describe参数并无实际意义,只是增加了元素定义的可读性
4、定义一组元素
当我们要定位一组元素时,可以使用PageElements类。
from poium import Page, PageElement
class ResultPage(Page):
# 定位一组元素
search_result = PageElements(xpath="//div/h3/a")
poium极大地简化了Page层的定义,除此之外,它还提供了很多的API,如PageSelect类简化了下拉框的处理等。读者可以到GitHub项目中查看相关信息。目前,poium已经在Web自动化项目中得到了很好的应用。