之前学习了selenium的脚本录制和webdriver的API,在进行脚本录制导出的脚本中,多了很多代码,这些代码就是unittest测试框架;
unittest框架介绍
unittest是Python单元测试框架,它提供了创建测试用例,测试套件以及批量执行的方案,在安装Python之后就自带了unittest,直接import unittest就可以使用;
unittest是可以对程序最小模块的一种敏捷化的测试,利用单元测试框架,创建一个类,这个类继承unittest的TestCase,这样可以把每个case看成是一个最小的单元,由测试容器组织起来,直接执行,并且可以引入测试报告;
如图:
Test Fixture:测试固件,初始化和清理测试环境,例如下面实现的setUp和tearDown都是测试固件,每个类中都应该抱哈这两个函数;
test case:单元测试用例,TestCase是编写单元测试用例最常用的类;
test suite:单元测试用例集合,TestSuite是最常用的类,测试方法组合在一起就是测试套件;
test runner:执行单元测试,批量执行测试套件,要注意不同于生成测试报告的runner;
test report:生成测试报告(可以省略)
例如我们实现一个脚本:
from selenium import webdriver
import time
import unittest
class Baidu1(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "http://www.baidu.com"
self.verificationErrors=[]
self.accept_next_alert = True
def tearDown(self):
self.driver.quit()
def test_hao(self):#实现点击hao123进入
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text("hao123").click()
time.sleep(8)
def test_wang(self):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_id("kw").send_keys("王一博")
driver.find_element_by_id("su").click()
time.sleep(6)
def test_bu(self):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_id("kw").send_keys("布拉格")
driver.find_element_by_id("su").click()
time.sleep(6)
if __name__ == "__main__":
unittest.main(verbosity=2)
#增加verbosity参数,这样测试的结果就会更加详细
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
对这个代码进行一个解释,Setup和tearDown方法是测试固件,是不能缺少的(相当于C++中的构造函数和析构函数),接下来分别实现各个测试用例,我这里实现了三个测试用例,也就是点击hao123进入、搜索王一博进入、搜索布拉格进入;这里要注意,每次执行一个用例,先调用setUp函数,然后执行,然后调用tearDown函数;
编写测试用例的函数必须以test_开头进行命名,执行时会主动执行这些测试用例,按时类中定义的方法不会主动执行,类中函数的执行顺序以ASCII码值进行排序;
这里要介绍这个verbosity参数:
在主函数中直接调用main(),在main中加入verbosity参数,测试结果会更详细;
verbosity=0:静默模式,只能获得总得测试用例和总的结果,比如总共5个测试用例,失败1,成功4
verbosity=1:默认模式,在每个成功的用例面前有个".",每个失败的用例前有个"F";
verbosity=2:详细模式,测试结果会显示每个测试用例的所有信息
我们知道上述需要编写多个测试用例才能对软件某个功能进行比较完整的测试,这些相关测试用例称为一个测试用例集,在unittest中用TestSuite类来表示;
批量执行脚本
- addTest
例如我们编写了TestBaidu1和TestBaidu2两个文件,怎么同时执行这两个文件,例如:
TestBaidu1:
from selenium import webdriver
import time
import unittest
class Baidu1(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "http://www.baidu.com"
self.verificationErrors=[]
self.accept_next_alert = True
def tearDown(self):
self.driver.quit()
def test_hao(self):#实现点击hao123进入
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text("hao123").click()
time.sleep(8)
def test_wang(self):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_id("kw").send_keys("王一博")
driver.find_element_by_id("su").click()
time.sleep(6)
def test_bu(self):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_id("kw").send_keys("布拉格")
driver.find_element_by_id("su").click()
time.sleep(6)
if __name__ == "__main__":
unittest.main(verbosity=2)
#增加verbosity参数,这样测试的结果就会更加详细
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
TestBaidu2:
from selenium import webdriver
import time
import unittest
class Baidu2(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "http://www.baidu.com"
self.verificationErrors=[]
self.accept_next_alert = True
def tearDown(self):
self.driver.quit()
def test_tie(self):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text("贴吧").click()
time.sleep(3)
def test_ba(self):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_id("kw").send_keys("巴黎")
driver.find_element_by_id("su").click()
time.sleep(6)
def test_ma(self):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_id("kw").send_keys("芒种")
driver.find_element_by_id("su").click()
time.sleep(6)
if __name__ == "__main__":
unittest.main(verbosity=2)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
因此有多个测试用例时,需要一个测试套件,将测试用例放在该套件中执行,unittest模块提供了TestSuite类来生成测试套件,使用该类的构造函数可以生成一个测试套件的实例,该类提供了addTest来把每个测试用例加入到测试套件中,将上述的测试模块文件与接下来要实现的脚本文件放在一个文件夹中,接下来实现:
run.py
import TestBaidu1
import TestBaidu2
import time
def createsuite():
suite = unittest.TestSuite()
#将测试用例加入到测试套件中
suite.addTest(TestBaidu1.Baidu1("test_wang"))#测试搜索王一博进入
suite.addTest(TestBaidu1.Baidu1("test_hao"))#测试点击hao123进入
suite.addTest(TestBaidu1.Baidu1("test_bu"))#测试搜索布拉格进入
suite.addTest(TestBaidu2.Baidu2("test_ba"))#测试搜索巴黎进入
suite.addTest(TestBaidu2.Baidu2("test_ma"))#测试搜索芒种进入
suite.addTest(TestBaidu2.Baidu2("test_tie"))#测试点击贴吧进入
return suite
if __name__ == "__main__":
suite = createsuite()#创建一个实例
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
它的运行结果就是:
使用addTest它的运行顺序就是按照添加的顺序依次执行;
使用addTest方法有一些缺点:
(1)需要导入所有的py文件,每新增一个需要导入一个;
(2)addTest需要增加所有的测试用例,有多少个测试用例,就需要增加多少个;
因此需要使用makeSuite()
- makeSuite
还是TestBaidu1和TestBaidu2这两个脚本,要共同执行,实现run.py,脚本如下:
import unittest
#导入TestBaidu1和TestBaidu2
import TestBaidu1
import TestBaidu2
import time
def createsuite():
suite = unittest.TestSuite()
#使用makeSuite
suite.addTest(unittest.makeSuite(TestBaidu1.Baidu1))
suite.addTest(unittest.makeSuite(TestBaidu2.Baidu2))
return suite
if __name__ == "__main__":
suite = createsuite()#创建一个实例
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
它的运行结果就是:
makeSuit是将测试用例类所有的测试用例组成的测试套件TestSuite,unittest调用makeSuite,只需要将测试类名称传入;
- TestLoader
TestLoader用于创建类和模块的测试套件,一般使用TestLoader().loadTestsFromCase(TestClass)来加载测试类
实现脚本:
run.py
import unittest
#导入TestBaidu1和TestBaidu2
import TestBaidu1
import TestBaidu2
import time
def createsuite():
suite1=unittest.TestLoader().loadTestsFromTestCase(TestBaidu1.Baidu1)#先得到TestLoader并且加载类
suite2=unittest.TestLoader().loadTestsFromTestCase(TestBaidu2.Baidu2)
suite=unittest.TestSuite([suite1,suite2])#添加到整体的套件中
return suite
if __name__ == "__main__":
suite = createsuite()#创建一个实例
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
运行结果是:
makeSuite和TestLoader的作用是一样的,但是他们的实现方式是不同的,makeSuite是先生成套件然后一个一个添加,而TestLoader是先导入然后再加到测试套件中;
但是使用前面的addTest、makeSuite、TestLoader方法都要将文件导入进来,而且都要将类导入进来,当文件很多时,类导入的代码也就多了,写多了就容易出错,因此提出了discover方法;
- discover
discover通过递归的方式到其子目录中从指定的目录开始,找到所有测试模块并返回一个包含他们对象的TestSuite,然后进行加载与模式匹配唯一的测试文件,例如实现脚本:
import unittest
def createsuite():
#使用discover
discovers=unittest.defaultTestLoader.discover("../-Python-",pattern="TestBaidu*.py",top_level_dir=None)
print(discovers)
return discovers
if __name__ == "__main__":
suite = createsuite()#创建一个实例
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
运行结果就是:
因此一般如果需要运行所有测试模块的测试用例,就用discover方法,如果需要运行某个测试模块的某个类的测试用例,就用makeSuite或者TestLoader方法,如果需要具体运行那个测试模块的测试类的某个测试用例,就用addTest方法;
如果在某个测试类中,想不执行某个测试用例,可以使用@unittest.skip(“skipping”)标签
- @unittest.skip(“skipping”)标签
例如不想执行TestBaidu1里面Baidu1类中的test_wang测试用例,就可以如图:
它最终运行run.py的运行结果就是:
这个一般是进行调试测试用例使用的,还有一点是如果将鼠标放在某个测试用例那里然后鼠标右键点击运行,只会运行这一个测试用例;
unittest断言
对于每一个case来说,一个case的执行结果一定有期望结果和实际结果,来判断case是通过还是失败,在unittest中提供了大量的方法来检查实际值和期望值,例如下面有一些常用断言:
1、assertEqual(arg1,arg2,msg=None():验证arg1=arg2,不相等则fail;
2、assertNotEqual(arg1,arg2,msg=None):验证arg1!=arg2,相等则fail;
3、assertTrue(expr,msg=None):验证expr是true,如果为false,则fail;
4、assertFalse(expr,msg=None):验证expr是false,如果为true,则fail;
(1)例如实现加assertEqual断言,如图:
(2)实现添加assertNotEqual断言,如图:
(3)实现添加assertFalse断言,如图:
HTML报告生成
在执行完脚本之后,需要看到HTML报告,通过HTMLTestRunner.py来生成测试报告(一般HTMLTestRunner支持Python2.0,而我这里用的是Python3.8版本,因此要使用最新的HTMLTestRunner,修改地址:https://blog.51cto.com/hzqldjb/1590802)
下载完成之后,将HTMLTestRunner.py放在Python安装目录下的Lib目录中即可
实现脚本:
import unittest import sys import os import HTMLTestRunner import time def createsuite(): discovers=unittest.defaultTestLoader.discover("../-Python-",pattern="TestBaidu*.py",top_level_dir=None) print(discovers) return discovers if __name__ == "__main__": # suite = createsuite()#创建一个实例 # runner = unittest.TextTestRunner(verbosity=2) # runner.run(suite) curpath = sys.path[0]#sys.path是Python的搜索模块的路径集,返回的结果是一个List
if not os.path.exists(curpath+'/resultreport'):#看是否有这个文件夹,如果没有就创建这个文件夹 os.makedirs(curpath+'/resultreport') #表示年-月-日-小时 分钟 秒格式生成当地时间 now = time.strftime("%Y-%m-%d-%H %M %S",time.localtime(time.time())) print("-----------------------") print(time.time()) print("-----------------------") print(time.localtime(time.time())) print("-----------------------") print(now) filename = curpath+'/resultreport/' + now + 'resultreport.html' with open(filename,'wb') as fp: runner=HTMLTestRunner.HTMLTestRunner(stream=fp,title="测试报告",description="测试用例",verbosity=2) suite=createsuite() runner.run(suite)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
运行结果如图:
异常与错误截图
在执行测试用例时,不可能每一次运行都成功,如果可以捕捉到错误,并且把错误截图保存,会给我们的错误定位带来方便,例如:
def test_hao(self):#实现点击hao123进入
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text("hao123").click()
# 异常和错误截图
try:
self.assertEqual(u"hao_上网从这里开始",driver.title,msg="not equal!!!")
except:
self.savescreenshot(driver,"hao.png")
time.sleep(8)
def savescreenshot(selfself,driver,file_name):
if not os.path.exists('./image'):
os.makedirs('./image')
now=time.strftime("%Y%m%d-%H%M%S",time.localtime(time.time()))
#截图保存
driver.get_screenshot_as_file('./image/'+now+'-'+file_name)
time.sleep(2)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
其中get_screenshot_as_file表示获取当前界面的截图
运行结果如下:
可以看到当前目录生成了image,可以打开它,如图:
数据驱动
之前我们都是将数据和代码一起编写,但是要考虑重复代码的情况,例如百度搜索“王一博”、“iu”、“西城男孩”,其实除了数据不同,代码都是相同的,因此就需要使用到ddt数据驱动,安装ddt之后就可以使用了;
ddt的使用
(1)使用@data标签传入多个参数
例如实现脚本:
from selenium import webdriver
import time
import os
from ddt import ddt,data,unpack,file_data
import unittest
@ddt#引入ddt
class Testddt(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "http://www.baidu.com"
self.verificationErrors = []
self.accept_next_alert = True
def tearDown(self):
self.driver.quit()
@data("王一博","iu","西城男孩")
def test_hao(self,value):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_id("kw").send_keys(value)
driver.find_element_by_id("su").click()
if __name__ == "__main__":
unittest.main(verbosity=2)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
他就可以搜索这三个数据,运行结果如图:
这样就可以减少重复代码的编写
(2)使用@(*getCsv(txt文件名))标签读txt文件并传入
例如我们有一个txt文件,内容如图:
注意第一行先写上data,并且要utf-8格式的,防止因为汉字导致乱码,然后要对sun和iu进行搜索,搜索sun的期待的是sun_百度搜索,iu期待的是iu_百度搜索,实现脚本:
from selenium import webdriver
import time
import os
from ddt import ddt, data, unpack, file_data
import unittest
import csv, sys
#实现getCsv方法
def getCsv(file_name):#实现作为txt文件的标签的getCsv方法
rows = []#定义一个数组,存放的是作为在test_hao用到的参数
path = sys.path[0]#list,返回的是一个数组,一系列路径,下标为0的数组的元素为当前文件所在的路径
print(path)
with open(path+'/data/'+file_name,'rt') as f:#将这个路径中的文件打开进行读(因为我的txt文件放在这个路径下)
readers = csv.reader(f, delimiter=',', quotechar='|')#文件中的内容分隔符是逗号,这个根据txt文件内容决定
next(readers, None)
#一行一行读放到readers中
for row in readers:
temprows=[]
for i in row:
temprows.append(i)#读取到的第一行的数据放在temprows中
rows.append(temprows)#temprows放到rows中,也就是rows的元素就是一行一行的数据
return rows#返回到test_hao方法中
@ddt#引入ddt
class Testddt(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "http://www.baidu.com"
self.verificationErrors = []
self.accept_next_alert = True
def tearDown(self):
self.driver.quit()
#作为txt文件标签
@data(*getCsv('test_baidu.txt'))
#([sun,sun_百度搜索],[iu,iu_百度搜索])
@unpack#当一次性要传一组数据而不是一个数据,就需要使用@unpack标签
def test_hao(self, value, expected_value):
driver = self.driver
driver.get(self.base_url+"/")
driver.find_element_by_id("kw").clear()
driver.find_element_by_id("kw").send_keys(value)
driver.find_element_by_id("su").click()
time.sleep(8)
print(value)
self.assertEqual(expected_value, driver.title)#加上断言,期望值是否是页面标题
print(expected_value)
print(driver.title)
if __name__ == "__main__":
unittest.main(verbosity=2)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
此时就可以搜索处sun和iu;
(3)使用@file_data标签读json串并传入
这里将json文件放在与我的工程相同路径下以便使用,json文件的内容如图:
实现脚本:
from selenium import webdriver
import time
import os
from ddt import ddt, data, unpack, file_data
import unittest
@ddt#引入ddt
class Testddt(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "http://www.baidu.com"
self.verificationErrors = []
self.accept_next_alert = True
def tearDown(self):
self.driver.quit()
@file_data('test_baidu.json')
def test_hao(self, value):
driver = self.driver
driver.get(self.base_url+"/")
driver.find_element_by_id("kw").clear()
driver.find_element_by_id("kw").send_keys(value)
driver.find_element_by_id("su").click()
time.sleep(8)
print(value)
if __name__ == "__main__":
unittest.main(verbosity=2)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
由于json文件不是一组一组的数据,而是一个一个的数据,所以不需要@unpack标签,运行结果如下:
(4)使用@data([],[],[]…)标签或者@data((),(),()…)来传入多组参数,例如实现脚本:
from selenium import webdriver
import time
import os
from ddt import ddt, data, unpack, file_data
import unittest
@ddt#引入ddt
class Testddt(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "http://www.baidu.com"
self.verificationErrors = []
self.accept_next_alert = True
def tearDown(self):
self.driver.quit()
@data([u"王一博",u"王一博_百度搜索"],["Lisa",u"Lisa_百度搜索"],["iu",u"iu_百度搜索"])
@unpack
def test_hao(self, value,expected_value):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_id("kw").clear()
driver.find_element_by_id("kw").send_keys(value)
driver.find_element_by_id("su").click()
time.sleep(8)
self.assertEqual(expected_value, driver.title) # 看是否与driver的title匹配
print(value)
print(expected_value)
if __name__ == "__main__":
unittest.main(verbosity=2)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
他也可以运行得到想要的结果,如图: