Selenium中的PageObject设计模式(Python3.6实现) 附Selenium中等待的问题
认识UI测试
个人兴趣接触了Selenium框架,刚开始的时候想必大家对这个框架还都蛮感兴趣的,放这个金字塔的意思就是,还是提醒一下,多进行基础的UnitTest(底层的单元测试),不要过分迷恋应用层的自动化框架。因为UI层如果全部实现自动化,是一个劳民伤财的事情,并且回报率很低,多关注底层的自动化测试才是明智之举。最近读了How Google Test(谷歌测试之道),书中对web产品测试就分为敏捷测试金字塔的三层,并且工作量配比是,Unit:70%,Service:20%,UI:10%。
PageObject设计模式(Python3.6)
PageObject模式主要是将每个页面设计为一个class,其中包含页面中的需要测试的元素(按钮,输入框,标题),等这样在Selenium测试页面中可以通过调取页面类来获取页面元素,这样避免当页面元素id或者位置变化后,需要改测试代码。当元素ID变化时,只需要改测试页面Class中的页面属性即可。将页面定位与和业务分开,分离测试对象(元素对象)和测试脚本(用例脚本),提高可维护性。
当我们对一个网页站看自动化UI测试的时候,初学者熟悉Selenium中的各种API调用以及使用方法的学习路线是很常见的,但是我们刚开始写的Code都是线性的UI流程。在熟悉了API的使用方法后,如果给你一个网站或者TestCase可能你关注的问题已经不是如何摸着线性的逻辑展开测试,而是如何解耦。
简单说一下我理解的框架的意义,我们刚开始接触一些设计模式MVC或者Django用到的MTV也好,都是一些框架使用起来确实方便但是为什么呢?初期只是觉得可能框架里面的API更丰富,实现了很多接口不需要我们自己写一些函数。但是更深层次的意义就在于解耦。把一个大到一个项目,小到一个网页的内容全部解耦。
UI自动化中的解耦
一个很明显的道理,比如一个网站迭代特别快,那么前端的JS代码的更改量肯定也是很大的,这个时候使用线性的元素定位就不是那么理智了,每次更改JS代码,你需要在你的Code中找到该部分然后再修改。这样业务层跟定位在一起的线形设计模式势必只适合一些简单的需求或者自己写的小Demo,所以我们就要把UI自动化最核心的问题定位,跟业务层解耦出来,单独处理定位的问题,然后在实现业务层。所使用到的方法是对一个UI页面做测试我们抽取为三层,举例为BasePage+LoginPage+Unittest。
BasePage
封装所有页面的公共方法。
例如:在__init__中定义WebDriver,基本的URL,title等
重写open方法,调用类内置方法_open(类私有的方法不能被import)
重写find_element方法(使用显示等待+异常判断(是否找到目标元素))
重写send_keys等
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage(object):
'''基础类,实际界面继承该类'''
def __init__(self,selenium_driver,base_url,title):
self.base_url=base_url
self.driver=selenium_driver
#超时等待时间
self.timeoout=30
def on_page(self,title):
'''获取当前页面title,检查输入的title是否在当前页面中,返回比较结果'''
return title in self.driver.title
#封装类私有方法_open
def _open(self,base_url,title):
'''默认使用get请求'''
self.driver.get(base_url)
self.driver.maximize_window()
#使用assert+on_page方法
assert self.on_page(title),u'打开网页失败'%base_url
#重写open方法
def open(self):
self._open(self.base_url,self.title)
#重写元素定位
def find_element(self,*loc):
try:
WebDriverWait(self.driver,10).until(EC.visibility_of_element_located(loc))
return self.driver.find_element(*loc)
except:
print('%s页面未找到%s元素')%(self,loc)
#重写send_keys
def send_keys(self,loc,value,clear_first=True,click_first=True):
try:
#getattr相当于实现self.loc
loc=getattr(self,'_%s'%loc)
if clear_first:
self.find_element(*loc).click()
self.find_element(*loc).send_keys(value)
except AttributeError:
print('页面中未能找到%s元素'%(self,loc))
注:Selenium中的三种等待
1.强制等待(sleep)
由time.sleep实现,此方法为强制等待让程序暂停一会然后继续执行。缺点:无法预知目标元素是否完全加载,设置的等待时间或长或短,影响Testcase运行质量与时长
2.隐式等待(implicitly wait)
如果设置隐性等待N秒,这个时间不是一个固定的时间如果在N秒内加载完成则执行下一步,未加载完成就报超时加载。特点:它并不针对某个页面元素进行等待,而是针对整个页面,在规定时间内以轮询的方式进行判断。 缺点:不智能,如目标为ajax局部刷新已经完成,就不需要等待页面全部加载完成
3.带有同步条件的等待–显示等待(WebDriverWait)
可以设置timeout时间以及轮询间隔+util/not uitl同步条件判断某个元素是否被加载出来更加灵活
LoginPage类
`from selenium.webdriver.common.by import By
import 刚刚的project下的BasePage
继承LoginPage类
class LoginPage(BasePage):
#定位器,通过元素属性定位元素对象
uname_loc=(By.NAME,'email')
pwd_loc=(By.NAME,'PWD')
submit_loc = (By.ID, 'dologin')
span_loc = (By.CSS_SELECTOR, "div.error-tt>p")
userid_loc = (By.ID, "spnUid")
``
#使用Overiding继承覆盖:如果父子类有一样名的方法,优先使用子类自己的方法
#调用BasePage中的_open打开链接
def open(self):
self._open(self.base_url,self.title)
#输入用户名,调用find_element方法,输入用户名
def input_uname(self,uname):
self.find_element(*self.uname_loc).send_keys(uname)
#输入密码
def iput_pwd(self,pwd):
self.find_element(*self.pwd_loc).send_keys(pwd)
#提交
def submit_click(self):
self.find_element(*self.submit_loc).click()
#显示状态
def show_span(self):
return self.fine_element(*self.span_loc).text
#登陆后用户ID查找
def show_userId(self):
return self.find_element(*self.userid_loc).text`
注:使用By来做定位器,然后把定位器传给find_element方法
Unittest case
import unittest
from xxx import LoginPage
from selenium import webdriver
class Caselogin126mail(unittest.TestCase):
"""
登录邮箱的case
"""
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.url = "http://mail.126.com"
self.username = "XXX"
self.password = "XXX"
# 用例执行体
def test_login_mail(self):
# 声明LoginPage类对象
login_page = LoginPage(self.driver, self.url, u"网易")
# 调用打开页面组件
login_page.open()
# 调用用户名输入组件
login_page.input_username(self.username)
# 调用密码输入组件
login_page.input_password(self.password)
# 调用点击登录按钮组件
login_page.click_submit()
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main()
解耦到此结束,前端页面定位元素发生变化只需要改定位器中的东西,其实用什么框架都是为了把定位元素从业务中抽取出来u,以免太大的修改量,影响测试进度。