页面对象模式是实施智能自动检查的关键。下面将介绍Python程序员是如何利用它的。
自动检查在UI level在某些圈内获得了不好的名声,主要是由于相关的维护费用。坏的名声在很大程度上算是实至名归。几乎每个做UI自动化的人都熟悉一些类似“UI分支下将用户名框重命名为了登录名框,所以现在一切都破损了”的情况。页面上的元素总是处于变化的状态,也应该是这状态。如果不是,那么你的产品已经停止了进展。我们所面临的挑战是应对这些变化,以减小维护的负担。
作为一名自动化从业者以及顾问,我发现处理这些变化的最简单的方法是把经常使用的那些命令和操作捆绑起来放入函数。你的脚本就会变成提取操作的框架调用和特定应用程序的组合。
脆弱还是更好?
比较这两个Selenium脚本的Python片段。
脆弱:
s.open("/login") s.type("username", "pragmatic") s.type("password", "magazine") s.click("login") s.wait_for_page_to_load("30000")
更好:
def login(s, username, password): s.type("username", username) s.type("password", password) s.click("login") s.wait_for_page_to_load("30000") # s.open("/login") s.login(s, "pragmatic", "magazine")
当一切都正常工作时,脆弱的那个例子实际上是更有效的,因为少了一个函数调用。但只要任一字段或按钮改变或流程以其他方式改变时,更好的那个例子就好多了。尤其是当你重复在所有测试中使用login功能。一个改变,多个维护的脚本:它是自动化维护的必杀技。
辅助函数不应当仅仅局限于动作。它们也可用于与列表框,单选按钮组等的相互作用
def select_country_by_label(s, label): s.select("country", "label=%s" % label)
这种风格的脚本设计适合自然脚本语言如Perl、Ruby和Python,通常更多是面向功能,而不像c#或者Java等面向对象语言。
下面进入页面对象。
页面对象模式
页面对象模式是出自webdriver项目,为了将你软件中的的独立页面的意图以及交互封装为一个对象。页面对象模式的最初的运用是在Java中,但最近的参考实现都出现了Java,C#和Ruby。然而尽我所知,Python没有这样的例子。到目前为止,本文的剩余部分将显示我是如何成功地在Python中使用了页面对象模式。
我们要做的第一件事就是对页面本身,以及页面上的元素进行区分。反过来这种差异将决定我们的继承模型。
- 页面包含实现方法(例如:login_page.submit())以及网络元素这两部分,其本身就是分开的对象。
- 网络元素是页面元素用来进行交互操作或者检查操作的结果——例如复选框,文本框,单选按钮等。
Object +-- unittest.TestCase +-- BasePageObject +-- BasePageElement
这时候我们就应该稍微多花点时间研究一下这些文件在磁盘上是如何存储的。当通过Python实现页面对象时,我发现它是利用包来整理的。因此,我们的两个基类就是通过如下形式存储的:
pageobjects/ __init__.py basepageobject.py basepageelement.py
当前,basePageObject类除了存在以外没有做任何事情。baseWebElement类看起来会在短期内被删除。
import unittest class basePageObject(unittest.TestCase): pass
这使得我们可以对我们的实际页面对象使用它,作为一个超类来获取所有与某个TestCase有关的断言。这里举一个存在于大多数系统的页面对象:登录页面。
from pageobjects import locators, selenium_server_connection from pageobjects.basepageobject import BasePageObject from pageobjects.basepageelement import BasePageElement class UsernameElement(BasePageElement): # def __init__(self): self.locator = locators["login.username"] # def __set__(self, obj, val): se = selenium_server_connection.connection se.type(self.locator, val) # class PasswordElement(BasePageElement): # def __init__(self): self.locator = locators["login.password"] # def __set__(self, obj, val): se = selenium_server_connection.connection se.type(self.locator, val) # class LoginPageObject(BasePageObject): # username = UsernameElement() password = PasswordElement() # def __init__(self, se): self.se = se self.se.open("/login") self.assertEqual("My Application - Login", self.se.get_title()) # def submit(self): wait_for = "selenium.browserbot.getCurrentWindow().document.getElementById ('LogoutButton')" self.se.click(locators["login.submit"]) self.se.wait_for_condition(wait_for, "30000")
不要在你的网页前面写上所有的元素类。相反,根据需求递增地完成,所以即使在登录页面可能会有一个Clear按钮和忘记密码的功能,无论是定义,还是脚本都暂时不需要他们。
从底下看起,LoginPageObject具有Submit操作,它被定义为一个类方法。当被调用时,它会点击一个按钮,并等待另一个按钮出现。其中Selenium按钮“点击”是为什么要将页面对象整理到包中的部分原因。
Python包给我们一个很好的点标记来引用封装内的模块,但如果你想要的东西是在主命名空间,你可以把它放在包的__init__.py内。在那其中应该有整个应用程序中的所有定位器(locator)的字典。是的,它的内存会变大,但是这也意味着在编辑时,它们是单一的点。locators = {} locators["login.username"] = "username" locators["login.password"] = "password" locators["login.submit"] = "login"
因为将有很多这样的对象,一个可以养成的好习惯就是使用page.thing作为键值。令键值成为描述该对象作用的东西,因为一旦它被使用为键值就不应该改变。然而它的值会随着用户界面开发而不断改变。
这两项恰好都在使用Selenium的标识定位器(默认),但他们也可以是使用XPath或CSS定位器字符串。
该LoginPageObject也有自己的自定义构造函数来连接Selenium RC服务器(在原来脚本所创建的),并将其设置为一个类属性。它还将导航到登录页面。另一种方式来写这个页面导航是先检查你是否已经登陆了页面,如果是就跳过它。def __init__(self, se): self.se = se try: self.assertEqual("My Application - Login", self.se.get_title()) except AssertionError: self.se.open("/login") self.se.wait_for_page_to_load("30000") self.assertEqual("My Application - Login", self.se.get_title())第二类构造函数更为有效的可以从您的应用程序访问页面,而不是直接访问。
(待续)
【英文原文:https://pragprog.com/magazines/2010-08/page-objects-in-python】
{测试窝原创译文,译者:大头}
译者简介:大头,在读日本九州大学修士,计算机专业,主研究方向为文本挖掘,及自然语言处理。