第 8 章 Page Object 设计模式- Selenium3 自动化测试
第 8 章 Page Object 设计模式
Page Object 是 UI 自动化测试项目开发实践的最佳设计模式(之一),主要特点体现在对界面交互细节的封装上,使测试用例更专注于业务的操作,从而提高测试用例的可读性。
8.1 认识 Page Object
Page Object 设计模式的优点如下:
- 减少代码的重复。
- 提高测试用例的可读性。
- 提高测试用例的可维护性,特别是针对UI频繁变化的项目。
当为 Web 页面编写测试类时,需要操作该 Web 页面上的元素。然而,如果在测试代码中直接操作 Web 页面上的HTML元素,那么这样的代码是极其脆弱的,因为 UI 会经常变动。我们可以将一个page对象封装成一个HTML页面(.py文件模块),然后通过提供的应用程序特定的API来操作页面元素,而不是直接在HTML中四处搜寻。
Page Object 原理如图 8-1 所示。
page 对象的一个基本经验法则是:凡是人能做的事,page 对象通过软件客户端都能做到。因此,它应当提供一个易于编程的接口,并隐藏窗口中底层的部件。当访问一个文本框时,应该通过一个访问方法(Accessor Method)来实现字符串的获取与返回,复选框应当使用布尔值,按钮应当被表示为行为导向的方法名。page 对象应当把在 GUI 控件上所有查询和操作数据的行为封装为方法。一个好的经验法则是,即使改变具体的前端页面元素,page 对象的接口也不应当发生变化。
尽管该术语是 page 对象,但并不意味着需要针对每个页面建立一个这样的对象。例如,页面上有重要意义的元素可以独立为一个 page 对象。经验法则的目的是通过给页面建模,使其对应用程序的使用者变得有意义。
Page Object 是一种设计模式,在自动化测试开发中应遵循这种设计模式来编写代码。
Page Object 应该遵循以下原则进行开发:
● Page Object 应该易于使用。
● 有清晰的设计结构,如 PageObjects 对应页面对象,PageModules 对应页面内容。
● 只写测试内容,不写基础内容。
● 在运行时选择浏览器,而不是类级别。
● 在测试方法中,不需要自己管理浏览器。
● 在测试类中,不需要直接接触 Selenium。
8.2 实现 Paget Object
下面我们将通过例子介绍这种设计模式的使用。
8.2.1 Paget Object 简单实例
以百度搜索为列,假设我们有如下测试代码。
这段代码最大的问题就是在三条测试用例中重复使用了元素的定位和操作。这会带来一个很大的问题,当元素的定位发生变化后,例如,id=kw 失效了,应及时调整定位方法,这时就需要在三条测试用例当中分别进行修改。假设,我们的自动化项目有几百条测试用例,而 UI 很可能是频繁变化的,那么就会提高自动化测试用例的维护成本。
Page Object 的设计思想上是把元素定位与元素操作进行分层,这样带的来最直接的好处就是当元素发生变化时,只需维护 page 层的元素定位,而不需要关心在哪些测试用例当中使用了这些元素。在编写测试用例时,也不需要关心元素是如何定位的。
创建 baidu_page.py 文件,内容如下。
首先,创建 BaiduPage 类,在__init__()实例化方法中接收参数 driver 并赋值给 self.driver。然后,分别封装 search_input()方法和 search_button()方法,定位并操作元素。
这里的封装只针对一个页面中可能会操作到的元素,原则上是一个元素封装成一个方法。当元素的定位方法发生改变时,只需维护这里的方法即可,而不需要关心这个方法被哪些测试用例使用了。
首先在测试模块中导入 BaiduPage 类,然后在每个测试用例中为 BaiduPage 类传入驱动对象(变量),这样就可以轻松地使用它封装的方法来设计具体的测试用例了。
这样做的目的就是在测试用例中消除元素定位。如果你要操作百度输入框,那么只需调用 search_input()方法并传入搜索关键字即可,并不需要关心百度输入框是如何定位的。
8.2.2 改进 Paget Object 封装
上面的例子演示了 Page Object 设计模式的基本原理,这样的分层确实带来了不少好处,但同时也带来了一些问题。例如,需要写更多的代码。以前一条测试用例只需写 4 到 5 行代码即可,现在却不得不先在 Page 层针对每个待操作的元素进行封装,然后再到具体的测试用例中引用。为了使 Page 层的封装更加方便简洁,我们做一些改进。
创建 base.py 文件,内容如下。
import time class BasePage: """ 基础pagen层,封装一些常用方法。 """ 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) # 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 # 执行JavaScript脚本 def js(self, script): self.driver.execute_script(script) # 封装休眠时间 def sleep(self, sec): time.sleep(sec)
创建 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_button()方法中使用了父类的 self.by_id()方法来定位元素,比原生的 Selenium 方法简短了不少。
在测试用例中,使用 BaiduPage 类及它所继承的父类中的方法。
test_baidu.py
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_case1(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 类时,只需将它传入浏览器驱动,就可以使用该类中提供的方法了。
8.3 poium 测试库
poium 是一个基于 Selenium/appium 的 Page Object 测试库,最大的特点是简化了 Page 层元素的定义。
项目地址:GitHub - SeldomQA/poium: Selenium/appium-based Page Objects test library
支持 pip 安装。
pip3 install poium==0.6.5
8.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 的思想,将元素定位与元素操作分层。
在测试用例中的使用如下。
import unittest from time import sleep from selenium import webdriver from poium_demo.baidu_page import BaiduPage class TestBaidu(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = webdriver.Chrome() def test_baidu_search_case1(self): page = BaiduPage(self.driver) page.get("https://www.baidu.com") 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() if __name__ == '__main__': unittest.main(verbosity=2)
首先导入 BiaduPage 类,传入浏览器驱动。然后,调用 get()方法访问 URL,该方法由 Page 类提供。接下来调用 BaiduPage 类中定义的元素对象,即 search_input 和 search_button,实现相应的输入和单击操作。
8.3.2 更多用法
想要更好地使用 poium,需要了解下面的一些使用技巧。
1.支持的定位方法
poium 支持 8 种定位方式。
from poium import Page, PageElement, PageElements class SomePage(Page): elem_id = PageElement(id_='id') elem_name = PageElement(name='name') elem_class = PageElement(class_='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, PageElements 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, PageElements class LoginPage(Page): """ 登录page类 """ username = PageElement(css='#loginAccount', describe="用户名") password = PageElement(css='#loginPwd', describe="密码") login_button = PageElement(css='#login_btn', describe="登录按钮") user_info = PageElement(css="a.nav_user_name > span", describe="用户信息")
需要强调的是,describe 参数并无实际意义,只是增加了元素定义的可读性。
4.定位一组元素
当我们要定位一组元素时,可以使用 PageElements 类。
from poium import Page, PageElement, PageElements class ResultPage(Page): # 定位一组元素 search_result = PageElements(xpath="//div/h3/a")
poium 极大地简化了 Page 层的定义,除此之外,它还提供了很多的 API,如 PageSelect 类简化了下拉框的处理等。读者可以到 GitHub 项目中查看相关信息。目前,poium 已经在 Web 自动化项目中得到了很好的应用。