从本章开始,我们将讲述如何实施自动化测试,在第一章的时候,我们也提供了自动化实施的步骤。那些儿步骤是指导方针,可以按着这一步步地去实施,可是有点儿笼统。本章我们将从具体实例入手,按前面的我们提到的步骤来讲解,通过本章的学习,你可以从一个被测试的网站着手,从零开始建立起普通的回归测试用例。
6.1 评审被测试对象功能
现在你的老大给你分配个任务:我们公司的业务已经成熟,网站主要功能也基本上不会变化了,我们想在每次上线后自动回归一下主功能。所以呢,我们想让你对我们的网站建立起自动化测试,用例要求覆盖主要功能。
当你接到这个任务的时候,你应该如何去做呢?不是去想用什么来写自动化,或是马上用你擅长的语言马上就去写测试脚本。而是要先分析一下被测网站的功能,哪些儿是主要的,哪些儿必须自动化,哪些儿不能自动化。
现在我们以众筹网(www.zhongchou.cn)为例,分析一下主要功能:
(1)登录注册
(2)浏览项目
(3)搜索项目
(4)查看项目详情
(5)支持项目
(6)个人信息设置
(7)发起项目
(8)查看自己相关的项目
(9)发起的项目管理和订单发货
(10)消息中心
等等,这十个主要功能是网站最基本的功能,然后我们分析一下哪些儿能自动化,哪些儿不能?
能自动化的部分:1,2,3,4,6,8,10
不能自动化的部分:5,7,9
为什么不能自动化呢?5,支持项目,涉及到真实的支付,影响项目对账,因为不管是测试环境还是线上环境,都是真实的支付,如果有支付的测试环境,也是可以自动化的。
7,发起项目,发起项目通过审核后,会在线上产生测试数据,影响用户体验。这违反了测试不能对线上产生垃圾数据的原则,所不不建议自动化,但是可以测试发起项目的流程,不对项目进行上线。
9,发起的项目管理和订单发货。发起项目我们就没有自动化,所以这块功能是没有数据的,再者订单发货会短信通知支持者,会影响到用户,不建议自动化。
当你接到任何一个测试任务的时候,需要先这样分析一下被测试对象的功能及是否合适自动化测试。然后查找一下是否存在对应功能的手工测试用例,一般公司都会做测试用例的收集工作的。如果没有,就先编写对应的测试用例,然后再去转化成自动化测试用例。
6.2 评审被测对象编码
评审完了主功能,然后开始评审被测试对象的编码,选择与被测试对象相同或是同一系列的脚本语言来写测试用例。一般网站编码无非是ASP.NET,PHP,JSP等,编写测试用例的脚本语言也可以用php,python,java等,根据实际情况,做出相应的选择。
经查看,我们的众筹网是用php编写的,按原则来说,我们应该用php来编写测试用例。Webdriver也支持php,但是php支持库不多,在用例组织和报告生成等方面也存在着不足,所以目前用php编写自动化测试用例的不多。综合考虑各种因素,我们决定用python来编写自动化测试用例。
6.3 自动化测试框架的选择
目前业界做页面自动化的都是选用selenium1.0 RC或是selenium2.0(webdriver),而它们之间是存在着区别的:
Selenium RC 工作原理:
(1)RC server 在服务端启动 浏览器 并将Core 注入到浏览器中 (为了解决浏览器的同源策略)
(2)我们的测试脚本调用Client API,Client将操作转化成标准的selenese语句发送给RC Server。
(3)Selenium Core 解释selenese语句,通过js的方式操作浏览器。
Webdriver 工作原理:
(1)WebDriver 启动目标浏览器,并绑定到指定端口。该启动的浏览器实例,做为web driver的remote server。
(2)Client 端通过CommandExcuter发送HTTPRequest 给remote server 的侦听端口(通信协议: the webriver wire protocol)。
(3)Remote server 需要依赖原生的浏览器组件(如:IEDriver.dll,chromedriver.exe),来转化转化浏览器的native调用。
那么remote server端的这些功能是如何实现的呢?答案是浏览器实现了webdriver的统一接口,这样client就可以通过统一的restful的接口去进行浏览器的自动化操作。目前webdriver支持ie, chrome, firefox, opera等主流浏览器,其主要原因是这些浏览器实现了webdriver约定的各种接口。
优缺点:
(1)RC 需要启动一个 RCserver,就直观感觉上多了一个步骤。
(2)RC 采用js 的方式,稳定性和兼容行方面还是取决与js的代码的质量。有时需要自己去写js代码来扩展相应的功能。由于自己是从RC用起的,对于RC js的方式还是比较接受,感觉比较灵活。
(3)至于弹出对话框和警告框的处理,RC原生是不支持的。不过可以通过第三方软件来解决,比如autoit。我用的就是autoit,使用简单方便,是一个不错的工具。
通过上面的分析,结合现在的时代潮流,我们采用高版本的selenium 2.0来作为我们的自动化测试的框架。
6.4 自动化测试用例运行环境
由于我们采用的是python开发的脚本语言,对运行环境要求不是太高,可是相应的包还是要安装的。在脚本编写期间,我们一般是在自己机器上编写和调试的,运行环境是windows环境。在后期的交付运行,自动执行回归的时候,我们要放到服务器上,服务器是linux环境的。
在Windows环境和linux环境下,python运行环境差别不大,所以不要太担心环境的问题。可是也有需要注意的地方,比如说文件路径的写法,两个环境下还是不同的。这也是我们一再要求用相对路径,最好用函数来获取路径,而不是把路径写死到代码中的原因。
6.5 自动化代码架构的规划
任何一个好的程序,都需要有好的规划。考虑到现在的结构化程序设计,我们的自动化测试代码也需要好好规划一下架构,不能像记流水帐似的按测试用例的步骤罗列下来。
6.5.1 代码架构规划的原因
自动化测试用例的主要工作就是回归测试,由于其任务的特殊性,就决定了自动化测试用例是成体系的,大里的自动化测试用例放在一起,如果不好好规划一下,会给后期的工作带来很大的影响。
代码规划的原因:
(1)增加可读性。根据用途,将代码进行分类存放,方便团队其他成员进行代码管理与合作开发。
(2)便于维护。自动化测试用例不可能是一成不变的,被测试的网站变化了,我们对应的自动化测试用例也要进行相应的维护。如果不进行规划,出现问题后不方便定位,更加会耗费大量的成本去维护。
(3)提高代码利用率。结构化程序设计,要求我们对代码重复利用率要高,同样的代码要写在公用函数或是方便,减少代码量。
6.5.2 如何规划代码架构
目前自动化的用法主要有两种:1,简单录制回放转化成的测试用例,此用例用于反复测试的功能点,一旦功能上线后便不再用。目地是加快回归测试的执行。特点就是对功能进行反复测试,不考虑通用性,健壮性等。2,用于回归测试的系统化的测试用例。此种用例是需要反复执行的,一定要考虑程序的通用性,健壮性以及执行时间等。
针对第一种测试用例,虽然使用时间比较短,通用率也不高,不过有些儿公司还是会收集这样的测试用例的,以便以后同样的或是类似的功能回归测试的时候用。此时可以按业务线对测试用例进行存放,测试用例中包含测试脚本和测试数据。
回归测试自动化测试用例是我们的重点,所以我们要详细规划一下:
(1)为了增加代码的通用性,我们把大部分测试用例需要执行的操作步骤封装成函数,放到公共的类中,并把公共的类也按功能进行分类,统一放到一个文件夹下,如CommonFunction.
(2)将所有的测试用例放到一个文件夹下,测试用例与测试数据分离开,以便网站有变化的时候,我们只需要修改相应该的测试数据即可。测试用例放到一个文件夹下,如:TestCases。
(3)测试数据放到和测试用例同名的Xml文件中,然后通过公用的数据读取方法,对测试数据进行读取,然后在测试用例中调用通用的函数,读取到的数据传递过去。所有数据存放到统一的文件夹下,如:TestData.
(4)测试用例集的存放。由于不同的测试目的,我们会运行不同的测试用例。如果测试目的A,我们将运行测试用例1,2,3;测试目的B ,我们运行测试用例1,3,4,5。所以我们会有不同的测试用例集文件,将这些儿文件统一存放到一个文件夹下,如:TestSuites.
(5)其他的资源文件,可以创建不同的文件夹来进行存放,如图片文件夹image.
所以,经过规划后,我们的测试用例结构如图6.5.2所示,图中的文件是我先前写的测试用例。下面的章节,我们会具体讲述测试用例:
图6.5.2自动化测试用例代码结构规划
6.6 编写自动化测试用例
我们规划好了代码架构,就可以编写具体的测试用例了。首先,我们写登录的测试用例,在写自动化测试用例的时候,我们要先写一下公用函数类。根据需要,我们写了三个通用的类放到CommonFunction文件夹下:WebDriverHelp用来存放所有页面操作用到公用方法;QT_Operations用来存放具体操作功能块,如登录,退出等;DataOperations用来存放所有数据操作,用来读取xml中的测试数据。
然后我们在TestCases文件夹下编写登录的测试用例TestCase_QT_Login.py执行具体的测试操作,验证测试用例执行是否成功。
并把测试用例需要的测试数据,放到TestData文件夹下TestCase_QT_Login.xml文件中。如果网站有所变化,可以修改这个里面的测试数据,从而减少代码维护的成本。
6.6.1 WebDriverHelp类的内容
WebDriverHelp是我们所有测试用例能用到的方法,可以在编写测试过程中不断抽象出来,写到这个类里面,相当于对Webdriver再次做了一下封装。
下面是部分代码,以供大家参考:
@author:songxianfeng
@copyright: V1.0
'''
import time
import datetime
from selenium import webdriver
fromselenium.webdriver.support.ui import Select
fromselenium.common.exceptions import NoSuchElementException
fromselenium.webdriver.common.action_chains import ActionChains
fromselenium.webdriver.common.keys import Keys
global G_WEBDRIVER,G_BROWSERTYTPE,DRIVER
class WebDriverHelp(object):
'''
本类主要完成页面的基本操作,如打开指定的URL,对页面上在元素进行操作等
'''
def __init__(self,btype="close",atype="firefox",ctype="local"):
'''
根据用户定制,打开对应的浏览器
@parambType: 开关参数,如果为close则关闭浏览器
@paramaType:打开浏览器的类型,如chrome,firefox,ie等要测试的浏览器类型
@paramcType:打开本地或是远程浏览器: local,本地;notlocal:远程 '''
global DRIVER
if( btype == "open" ):
if( atype == "chrome" ):
if(ctype == "local"):
DRIVER = webdriver.Chrome()
DRIVER.maximize_window()
elif(ctype == "notlocal"):
DRIVER =webdriver.Remote(command_executor='http://124.65.151.158:4444/wd/hub',desired_capabilities=webdriver.DesiredCapabilities.CHROME)
DRIVER.maximize_window()
elif( atype == "ie" ):
if(ctype == "local"):
DRIVER =webdriver.Ie()
DRIVER.maximize_window()
elif(ctype == "notlocal"):
DRIVER =webdriver.Remote(command_executor='http://124.65.151.158:4444/wd/hub',desired_capabilities=webdriver.DesiredCapabilities.INTERNETEXPLORER)
DRIVER.maximize_window()
elif( atype == "firefox" ):
if(ctype == "local"):
DRIVER =webdriver.Firefox()
DRIVER.maximize_window()
elif(ctype == "notlocal"):
DRIVER =webdriver.Remote(command_executor='http://10.20.5.56:4444/wd/hub',desired_capabilities=webdriver.DesiredCapabilities.FIREFOX)
DRIVER.maximize_window()
self.DRIVER = DRIVER
def setup(self,logintype):
'''
定制测试URL
@paramloginplace: 指定测试的URL: qiantai:前台测试地址,houtai:后台测试地址,zhengshi:正式环境测试地址
ysh:原始会测试地址 zhengshiysh:正式原始会测试地址
'''
try:
qiantai_url = "http://test.zhongchou.cn"
ysh_url = "http://test.ysh.zhongchou.cn"
houtai_url = "http://test.admin.zhongchou.cn"
zhengshi_url = "http://www.zhongchou.cn"
zhengshi_ysh_url = "http://ysh.zhongchou.cn"
if(logintype=="qiantai"):
self.DRIVER.get(qiantai_url)
elif(logintype=="houtai"):
self.DRIVER.get(houtai_url)
elif(logintype=="zhengshi"):
self.DRIVER.get(zhengshi_url)
elif(logintype=="ysh"):
self.DRIVER.get(ysh_url)
elif(logintype=="zhengshiysh"):
self.DRIVER.get(zhengshi_ysh_url)
else:
print'路径错误!'
self.DRIVER.implicitly_wait(1)
except NoSuchElementException:
print'您选择的测试地址出错!!'
def teardown(self):
'''
关闭浏览器
'''
self.DRIVER.quit()
def geturl(self,url):
'''
打开指定的网址
@param url:要打开的网址
'''
self.DRIVER.get(url)
def selectvalue(self,findby,select,selectvalue):
'''
通过定制定位方法和要选择项的文本,选择指定的项目
@paramfindby:定位方法,如:byid,byname,byclassname等
@paramselect: 要执行选择操作的下拉框句柄
@paramselectvalue: 下拉框中要选择项的文本
'''
if(findby == 'byid'):
select = Select(self.findelementbyid(select))
elif(findby =='byname'):
select = Select(self.findelementbyname(select))
elif(findby =='byclassname'):
select = Select(self.findelementbyclassname(select))
select.select_by_visible_text(selectvalue)
def inputvalue(self,findby,elmethod,value):
'''
通过定制定位方法,在输入框中输入值
@paramfindby: 定位方法,如:byid,byname,byclassname,byxpath等
@paramelmethod: 要定位元素的属性值,如:id,name,class name,xpath等
@paramvalue: 要给文本框输入的值
'''
if(findby == 'byid'):
self.findelementbyid(elmethod).send_keys(value)
elif(findby == 'byname'):
self.findelementbyname(elmethod).send_keys(value)
elif(findby =='byclassname'):
self.findelementbyclassname(elmethod).send_keys(value)
elif(findby == 'byxpath'):
self.findelementbyxpath(elmethod).send_keys(value)
……
上面的代码都有很详细的注释,在此就不一一讲述了。
6.6.2 QT_Operations类的内容
QT_Operations类是业务相关的公用功能块的封装,以便增加函数的公用性,减少代码量。例如,登录,在很多测试用例的第一步都需要先登录再操作的。所以你可以抽像出测试用例中的功能模块,封装后放到这个类中。
QT_Operations类的部分代码展示:
#-*- coding: UTF-8 -*-
'''
Created on2014-12-15
@author:songxianfeng
@copyright: V1.0
'''
import time
import win32api
import win32con
fromWebDriverHelp importWebDriverHelp
class QT_Operations(object):
'''
众筹前台相关操作
'''
def login(self,userName,passwd):
'''
从首页直接登录
@param userName: 用户名
@param passwd:密码
@param type1:指示登录方式,1为从主页登录,2,从登录页登录
'''
WebDriverHelp().clickitem("byclassname", "Js-showLogin")
time.sleep(3)
WebDriverHelp().clearvalue('byname','username')
WebDriverHelp().inputvalue('byname','username',userName)
WebDriverHelp().clearvalue('byname','user_pwd')
WebDriverHelp().inputvalue('byname','user_pwd',passwd)
time.sleep(1)
WebDriverHelp().clickitem("byclassname", "a-btn")
time.sleep(5)
def logout(self):
'''
退出登录
'''
WebDriverHelp().geturl("http://www.zhongchou.cn/usernew-loginout")
……
展示部分只包含了登录和退出功能,其他的功能可以根据测试用例的需要进行添加。
6.6.3 DataOperations类的内容
DataOperations类是对测试数据进行读取的操作,我们是用xml来存放测试数据的,所以测试用例执行的时候,需要先将测试数据读取出来,传递给相应的函数来对测试用例进行执行。然后根据执行的结果,判断测试用例是否执行成功。
DataOperations的内容:
#-*- coding: UTF-8 -*-
'''
Created on 2014-12-15
@author:songxianfeng
@copyright: V1.0
'''
import MySQLdb
from xml.dom import minidom
global DOC,CONN
class DataOperations(object):
'''
数据读取相关操作
'''
def __init__(self,filename):
'''
初始化xml文档
'''
global DOC,CONN
DOC = minidom.parse("../testData/"+filename)
def readxml(self,ftagname,num,stagname):
'''
从指定的文件中中读取指定节点的值
@paramftagname:起始节点的名称,如:project
@param num:取与起始节点相同的第num个节点
@param stagname:起始节点下的二级节点
@return: 返回二级节点的值
'''
root = DOC.documentElement
message=root.getElementsByTagName(ftagname)[num]
return message.getElementsByTagName(stagname)[0].childNodes[0].nodeValue
def readxml_attribute(self,ftagname,num,stagname,attributeName):
'''
从all_case.xml文件中读取节点的属性值
@paramftagname:起始节点的名称,如:project
@param num:取与起始节点相同的第num个节点
@paramstagname: 起始节点下的二级节点
@paramattributeName: 二级节点的属性名
@return:返回二级节点指定的属性值
'''
root = DOC.documentElement
message=root.getElementsByTagName(ftagname)[num]
returnmessage.getElementsByTagName(stagname)[0].getAttribute(attributeName)
python对xml的读取操作,如果你不太明白,可以去自行学习相关的内容,要教程不讲解相关的操作。
6.6.4 具体的测试用例
上面的公用方法类创建以后,我们就可以着手编写具体的测试用例了。在TestCases文件夹下创建测试用例TestCase_QT_Login.py,然后编写下面的测试步骤:
(1) 打开众筹网。
(2) 点击登录按钮,输入用户名和密码。
(3) 验证是否登录成功,用户昵称是不是刚刚登录的账号。
(4) 退出登录,关闭浏览器。
测试用例代码如下:
-*- coding: UTF-8 -*-
'''
Created on2014-12-15
@author:songxianfeng
@version: v1.0
'''
import unittest
import time
#导入需要的公共函数类
fromCommonFunction.WebDriverHelp import WebDriverHelp
fromCommonFunction.DataOperations import DataOperations
fromCommonFunction.QT_Operations import QT_Operations
class testcases_login(unittest.TestCase):
'''
登录检测
'''
def setUp(self):
WebDriverHelp("open","firefox","local").setup("zhengshi")#打开浏览器,并打开众筹网
def testlogin(self):
#登录检测
dataoper=DataOperations("TestCase_QT_Login.xml") #读取测试数据
#登录
QT_Operations().login(dataoper.readxml('login', 0, 'username'),
dataoper.readxml('login', 0, 'password'))
self.assertEqual(WebDriverHelp().gettext('byxpath',dataoper.readxml('login', 0, 'checkpoint1')),dataoper.readxml('login', 0, 'value1')) #判断登录是否成功
#退出
QT_Operations().logout()
time.sleep(5)
self.assertEqual(WebDriverHelp().gettext('byclassname',dataoper.readxml('login', 0, 'checkpoint2')),dataoper.readxml('login', 0, 'value2')) #判断退出是否成功
def tearDown(self):
WebDriverHelp().teardown()#关闭浏览器
if __name__ == "__main__":
unittest.main()
经过我们上面的封装,现在具体的测试用例只是传参数,调用具体的函数,验证执行结果。是不是非常简单?有点儿像我们小时候玩积木,用一块块现成的积木堆积出魔幻的城堡。
6.6.5 测试用例的具体数据
由于我们把测试用例和测试数据完全分离开了,所以我们用和测试用例同名的文件名命名对应的测试数据文件。登录测试用例的数据文件是TestCase_QT_Login.xml,具体内容如下:
<?xmlversion="1.0" encoding="UTF-8"?>
<TestData>
<login name="登录测试数据">
<username>183*****905</username>
<password>*******</password>
<checkpoint1>//div[@id='Js-header-loginBtn']/span/i[2]</checkpoint1>
<value1>潜龙0318</value1>
<checkpoint2>Js-showLogin</checkpoint2>
<value2>登录</value2>
</login>
</TestData>
我们将要验证的数据,获取验证元素的Xpath都写到这个里面,这样就算网页有所变化,我们只需要改这个数据文件中对应的Xpath即可,不需要更改测试用例。这样可以将网站变化影响到测试用例降到最低,同时也减少了我们维护测试用例的成本。
编写完上面所有的代码后,我们只要右击测试用例,选择Run as->python run,调试运行测试用例即可。
6.7 本章小结
本章我们讲解了如何从接触到一个网站开始,从零着手去编写自动化测试用例。通过上面各个方法的考虑,选择合适的框架,脚本语言,规划代码架构,编写公用函数,并以众筹网的登录注册为例,我们编写了具体的测试用例。通过这样的讲解,相信大家一定能编写测试用例了,要建立测试用例集,不过是按照先前我们选择的测试用例,全部转化成测试用例代码即可。第七章我们将讲述测试用例集的组织,以及将测试用例放到代码管理工具jenkins上实现自动化运行。从编写自动化测试用例,讲述到实现无人值守的回归测试用例的建立,希望你能学到点儿实用的东西。