前言
这一篇博客主要是来讲解自动化测试当中的单元测试框架unittest,这里的单元测试指的是对最小的软件设计单元(模块)进行验证,在UI单元测试里面,我们的单元测试主要针对UI界面的功能进行自动化测试,所以这里大家注意区分JAVA的Junit框架和unittest框架的区别。
一丶解析
unittest框架是python的单元测试框架,这里注意和白盒测试当中的单元测试区分,白盒测试当中单元测试框架使用的是Junit。作用主要如下:
1>提供用例组织和执行:当测试用例很少的时候,不用考虑组织这个问题。但是当测试用例达到了几百几千条的时候,就要注意扩展性和维护性的问题,也就是考虑用例的规范与组织问题。单元测试框架就是解决这个问题的。
2> 提供丰富的比较方法:在测试用例执行完之后都需要将实际结果和语气结果进行比较(断言),从而断定用例是否可以顺利通过。单元测试一般会提供丰富的断言方法
3>提供丰富的日志:在测试用例执行失败的时候可以清楚的抛出失败的原因。当所有的测试用例执行完成之后,能提供丰富的执行结果。比如:执行时间,失败用例数,成功用例数
这里需要特别提一下unnitest里面四个重要的概念
1.Test Fixture:对一个测试用例环境搭建和销毁,就是一个fixture,通过覆盖
setup()和tearDown()方法实现。
setup():可以进行测试环境的搭建,比如获取待测试浏览器的驱动,或者说测试的
过程中想要访问数据库,就可以在setup()中建立数据库连接来进行初始化
tearDown():此方法进行环境的销毁,可以关闭浏览器,关闭数据库连接,清除
数据库中产生的数据等操作。
2.Test Case:一个Test Case的实例就是一个测试用例。测试用例就是一个完整的
测试流程,包括测试前准备环境的搭建(setUp)丶实现测试过程的代码,以及测
试后环境的还原。这也是单元测试的精髓所在,一个测试用例就是一个完成的测试
单元,可以对某一个功能进行验证。
3.Test Suite:一个功能的验证往往需要多个测试用例,可以把多个测试用例集
合在一起执行,这就是TestSuite概念。Test Suit可以用来将多个测试集合组装
在一起执行。
4.TestRuner:测试的执行也是一个非常重要的概念,在unittest框架中,通
过TextTestRunner类提供的run()方法来执行test suit/test case。
所以总结一下,unittest框架为单元测试提供了创建测试用例,测试套件和批量执行的方案。作为单元测试的框架,unittest也可以是对程序最小模块的一种敏捷化的测试。自动化测试当中,我们虽然不需要做白盒测试,但是需要知道所使用语言的单元测试框架,利用单元测试框架把case组合起来变成一个类,一个继承了unittest的TestCase的类,一个case就是一个最小的单元。
二丶关于应用
这里我们的unittest框架python自带,使用时候直接继承就行。
1.测试套件
所谓测试套件,就是把不同文件里面,不同类里面的不同测试方法组织起来放在一起来运行。主要有以下几种
<1>addTest
这就是把不同文件,不同类里面的不同测试方法一个一个的加进去。类似这样子
import unittest
from 测试_2022.Month06.day22 import testbaidu1
from 测试_2022.Month06.day22 import testbaidu2
def creatSuit():
#要把不同的测试脚本的类中的需要执行的方法放在一个测试套件中
suit = unittest.TestSuite()
suit.addTest(testbaidu1.Baidu1("test_hao"))
suit.addTest(testbaidu2.Baidu2("test_hao"))
suit.addTest(testbaidu2.Baidu2("test_baidusearch"))
return suit
if __name__ == "__main__":
suit = creatSuit();
#verbersity= 0, 1, 2
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suit)
上面的testbaidu1和testbaidu2是我写的测试文件,其中的Baidu1和Baidu2就是测试类,其中的test_hao和test_baidusearch是里面的测试方法。
然后把他们一起放到一个测试套件里面,在testsuit.py中实现。但是这样子搞是很不方便的,这大大阻碍了脚本的执行效率。
1.需要导入py文件,就像最上面一样的导包一样,你每次在不同的.py文件里面进行测试方法的添加,就要导入一个新的包。
2.一次只能添加一个新的测试方法,如果说一个py文件里面有10个测试文件,如果都要组装到测试套件中,就需要增加10次
<2>makesuit() + TestLoader()的使用
makesuit()就是可以把测试用例类内所有的测试case组成测试套件TestSuite,unittest调用makeSuite的时候,只需要把测试类名称传入即可。
import unittest
from 测试_2022.Month06.day22 import testbaidu1
from 测试_2022.Month06.day22 import testbaidu2
def creatSuit():
# makeSuit
suit = unittest.TestSuite()
suit.addTest(unittest.makeSuite(testbaidu1.Baidu1))
suit.addTest(unittest.makeSuite(testbaidu2.Baidu2))
return suit
if __name__ == "__main__":
suit = creatSuit();
#verbersity= 0, 1, 2
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suit)
然后TestLoader用于创建类和模块的测试套件,一般的情况下使用TestLoader().loadTestsFromTestCase(TestClass)
来加载测试类。这种的话就是把类装入装入小的测试套件中,然后把小的测试套件装入最终的一个大的测试套件里面。
import unittest
from 测试_2022.Month06.day22 import testbaidu1
from 测试_2022.Month06.day22 import testbaidu2
def creatSuit():
suit1 = unittest.TestLoader().loadTestsFromTestCase(testbaidu1.Baidu1)
suit2 = unittest.TestLoader().loadTestsFromTestCase(testbaidu2.Baidu2)
suit = unittest.TestSuite([suit1, suit2])
return suit
if __name__ == "__main__":
suit = creatSuit();
#verbersity= 0, 1, 2
runner = unittest.TextTestRunner(verbosity=0)
runner.run(suit)
<3>discover()的应用
dicover是通过递归的方式到其子目录中从指定的目录开始,找到所有测试模块并返回一个包含它们对象的TestSuite,然后进行加载与模式匹配唯一的测试文件,discover参数分别为
discover(dir,pattern,top_level_dir=None)
这里唯一需要注意的就是路径问题
import unittest
from 测试_2022.Month06.day22 import testbaidu1
from 测试_2022.Month06.day22 import testbaidu2
def creatSuit():
discover = unittest.defaultTestLoader.discover("../day22", pattern="testbaidu*.py", top_level_dir=None)
return discover
if __name__ == "__main__":
suit = creatSuit();
#verbersity= 0, 1, 2
runner = unittest.TextTestRunner(verbosity=0)
runner.run(suit)
这里说一下哈,我第一个参数../day22
是我的指定的子目录,testbaidu*.py
是指定所有以testbaidu
开头的测试文件
<4>关于verbosity取值问题
这里取值我们取三个0,1,2
,越往后,测试用例显示的信息越详细,比如说报错,运行时间啊啥的。
0 ( 静默模式): 你只能获得总的测试用例数和总的结果比如总共100个失败,20 成功80
1 ( 默认模式): 非常类似静默模式只是在每个成功的用例前面有个“ . ” 每个失败的用例前面有个“F”
2 ( 详细模式): 测试结果会显示每个测试用例的所有相关的信息
2.测试用例的执行顺序
一般来说我们的测试用例是有执行顺序的,比如说下面这个例子:
def test_hao(self):
#省略内容
def test_haidusearch(self):
#省略内容
这两个测试用例,哪个先执行呢?怎么看呢?测试用例执行顺序如下:
先是0~9
再是A~Z
再是a~z
那么像上面中的,前面开头都一样怎么看呢?很简单,一个一个往后比就行了。
i在o前面,所以说下面的测试用例先执行。
所以, TestAdd 类会优先于TestBdd 类被发现, test_aaa() 方法会优先于test_ccc() 被执行。对于测试目录与测试文件来说, unittest 框架同样是按照这个规则来加载测试用例。
但是这里要说一下,addTest()方法按照增加顺序来执行
。
3.忽略测试用例的执行
有时候有些测试用例我们不想要去测试,这个时候我们就可以忽略它,让他不去执行,用起来也很简单,给不想执行的测试用例前面加上
@unittest.skip("skipping")
大致如下:
@unittest.skip("skipping")
def test_hao(self):
#测试内容略
4.unittest断言
在自动化测试当中,对于每个测试case来说,一个case的执行结果中,必然会有期望值和实际值,来判断当前的case是通过还是失败。在unittest库中,提供了大量的实用方法来检查预期值与实际值,来验证case的结果,一般来说,检查条件大体分为等价性,逻辑比较以及其他,如果给定的断言通过,测试会继续执行下一行的代码,如果断言失败,对应的测试case就会立刻停止,或者生成错误信息(一般打印错误信息即可),但是不要影响其他的case执行。这里比较常用的有以下几种
这里随便举个例子吧
self.assertNotEqual(driver.title, "突如其来的假期_百度搜索", msg="实际结果和预期结果一致" )
这的driver.title是在输入框输入搜索内容后跳转到对应搜索页面时候那个页面的名称,这里其实就是比较你的内容是不是不是“突然其来的假期”
如果不是“突如其来的假期”,那就是true
如果是“突如起来的假期”,那就是false
(PS:注意,我这里的断言方法是NotEqual哦)
5.HTML报告生成
如果说测试用例很少,那么测试报告看起来就没那么重要,但是脚本如果说很多,在执行完毕后HTML报告就显得尤为重要,所以这个时候我们就需要通过HTMLTestRunner.py来生成测试报告。
<1>前置工作
这个前置工作还是很简单的,来这个网站下载HTMLTestRunner.py文件。
下载地址
下载好之后,把他放在你的python文件目录下的lib目录下
<2>使用步骤
1>创建一个存放HTML的文件夹
首先就是创建一个存放HTML报告的文件夹,这一步的话就是
import os
if __name__=="__main__":
# 文件夹要创建在哪里
curpath = sys.path[0]
#这一步可以让你知道你的文件路径在哪里
print(sys.path)
print(sys.path[0])
# 创建文件夹,创建的这个文件夹干什么
if not os.path.exists(curpath+'/resultreport'):
os.makedirs(curpath+'/resultreport')
2>解决重复命名的原因
怎么解决重复命名的问题呢?最好的方式就是使用时间来命名,因为时间永远不会重复。
now = time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))
print(now)
print(time.time())
print(time.localtime(time.time()))
# 文件名
filename = curpath + '/resultreport/'+ now + 'resultreport.html'
3>报告的输出
最后一部分就是报告的输出了
# 打开HTML文件,wb以写的方式
with open(filename, 'wb') as fp:
# 括号里面的参数是HTML报告里面的参数
runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title=u"测试报告",
description=u"用例执行情况", verbosity=2)
suite = createsuite()
runner.run(suite)
<4>代码详解
所以总体代码如下:
import HTMLTestRunner
import os
import sys
import time
import unittest
def createsuite():
discovers = unittest.defaultTestLoader.discover("../day22", pattern="testbaidu*.py", top_level_dir=None)
print(discovers)
return discovers
if __name__=="__main__":
# 文件夹要创建在哪里
curpath = sys.path[0]
print(sys.path)
print(sys.path[0])
# 1,创建文件夹,创建的这个文件夹干什么
if not os.path.exists(curpath+'/resultreport'):
os.makedirs(curpath+'/resultreport')
# 2,文件夹的命名,不能让名称重复
# 时间 时分秒 ——》名称绝对不会重复
now = time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))
print(now)
print(time.time())
print(time.localtime(time.time()))
# 文件名
filename = curpath + '/resultreport/'+ now + 'resultreport.html'
# 打开HTML文件,wb以写的方式
with open(filename, 'wb') as fp:
# 括号里面的参数是HTML报告里面的参数
runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title=u"测试报告",
description=u"用例执行情况", verbosity=2)
suite = createsuite()
runner.run(suite)
这里的话还是再说一下关于其中一些代码的含义
1.curpath = sys.path[0]打印出来的路径是D:\PyCharm Community Edition 2021.2.3\PyCharmData\测试_2022\Month06\day22,也就是这个HTMLReport文件的路径。首先就是判断该文件夹是否存在,不在的话就在该路径底下创建一个resultReport的文件夹,来保存测试报告。
2.now = time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))是格式化时间,time.time()获取的是时间戳, time.localtime(time.time())将时间戳转化成现在的时间,time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))是将现在的时间格式化为年-月-日- 时 分 秒,能够使得测试报告的名字更加清晰。
3.with open(filename, 'wb') as fp:打开一个文件,以wb的形式去写入,fp为输入流。
4.runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title=u"测试报告",description=u"测试用例执行的结果", verbosity=2),stram是写入文件流,title是给该html文件起标题,description对测试的结果的描述,verbosity=2是对测试报告结果信息复杂度。
6.异常捕捉与错误截图
用例总会有失败的时候,所以如果可以捕捉到错误,并且把错误截图保存,这是一个很好的事情,也可以帮我们更方便的定位。
这里用一段代码来进行讲解吧,更能详细一些
def test_hbaidu(self):
driver = self.driver
url = self.url
driver.get(url)
# self.assertEqual("突如其来的假期_百度搜索", driver.title, msg="实际结果和预期结果不一致" )
self.assertTrue("百度一下,你就知道" == driver.title, msg ="不一致!!!")
driver.find_element_by_id("kw").send_keys("突如其来的假期")
driver.find_element_by_id("su").submit()
time.sleep(5)
print(driver.title)
try:
self.assertNotEqual(driver.title, "突如其来的假期_百度搜索", msg="实际结果和预期结果一致" )
except:
self.saveScreenAsPhoto(driver,'haohao.png') #这里是关于测试错误截图的,当预期结果和实际结果不一致,就进行断言并且截图
time.sleep(6)
def saveScreenAsPhoto(self, 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) #注意方法名,这个是错误截图的API
time.sleep(3)
先看test_hbaidu
,这段代码我有一段是抛出异常
try:
self.assertNotEqual(driver.title, "突如其来的假期_百度搜索", msg="实际结果和预期结果一致" )
except:
self.saveScreenAsPhoto(driver,'haohao.png') #这里是关于测试错误截图的,当预期结果和实际结果不一致,就进行断言并且截图
根据上面的代码显示,走到这里的时候,我用的断言方法是NotEqual,但是这里很明显它们是相等的,所以这里就会报错,但是这里我抛出了异常,也就是会跳转到saveScreenAsPhoto(self, driver, file_name):
这个方法,然后这里就会在这个我的文件夹下面去创建对应的错误截图
三丶数据驱动
这一部分很重要很重要,为什么说是数据驱动呢?因为之前我们的测试代码和要测试的数据都是放在一起写的,但是有时候需要测试的数据很多,或者说种类多,那你怎么搞?一个测试用例写一个?所以就很麻烦,所以有没有办法可以一次性全部搞定呢?有的,就是我们的数据驱动。python的unnitest框架没有数据驱动的功能,所以这里我们又想用unnitest,又想使用数据驱动功能,就要安装DDT。
1.安装DDT
这里我现在展示一下安装完成的界面
如果说没有安装的话
pip install ddt
在命令窗口使用上述命令就可以
这里安装完毕之后,DDT的使用方法可以参考下面的文档
2.导包
这里的话就是导入一些我们需要使用的工具包
from ddt import ddt,unpack,data,file_data
导完包之后,记得要在类上面使用标签@ddt
3.数据驱动的方式
1.@data(value)
这种的话就是一次性传一个参数,括号里面写参数,如下:
from selenium import webdriver
import unittest
import time
from ddt import ddt, unpack, data, file_data #这里是一些应用的标签
import sys, csv
@ddt
class myUnittest1(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.url = "https://www.baidu.com/index.php?tn=monline_3_dg"
self.driver.get(self.url)
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
self.driver.quit()
@data("王维","李白","张之洞")
def test_date1(self,value):
driver = self.driver
driver.get(self.url + "/")
driver.find_element_by_id("kw").send_keys(value)
driver.find_element_by_id("su").click
time.sleep(2)
if __name__ == "__main__":
unittest.main(verbosity=1)
2.@data(value1,value2…)
这种方式的话就是以一次性传递多个参数。
import time
import unittest
from ddt import ddt, unpack, data
from selenium import webdriver
@ddt
class myUnittest2(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.url = "https://www.baidu.com/index.php?tn=monline_3_dg"
self.driver.get(self.url)
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
self.driver.quit()
@data(['Lisa', u"Lisa_百度搜索"], [u"双笙", u"双笙_百度搜索"], [u"张一山", u"张一山_百度搜索"])
@unpack
def test_data(self,value,name):
driver = self.driver
driver.get(self.url)
driver.find_element_by_id("kw").clear()
driver.find_element_by_id("kw").send_keys(value)
time.sleep(1)
driver.find_element_by_id("su").click
time.sleep(2)
self.assertEqual(driver.title,name,msg="名称不同")
time.sleep(2)
if __name__ == '__main__':
unittest.main()
和上面的使用方式很相似,但是需要注意的就是这个标注
3.@file_data(“json文件”)
有些时候我们需要测试大量的数据,所以就不能直接在@data中直接输入了,这样会显得代码很乱,所以可以把上面的内容写入到一个json文件里面,以json的形式来保存数据。
(PS:这里我的文件和我博客中的测试文件是写在一起的)
所以代码中直接在测试用例前面添加@file_data("test_baidu_data.json")
就行,但是有一点要注意哈,就是你需要进行导包from ddt import file_data
,然后就可以使用js文件格式输入了,下面给出代码
还有我的js文件里面的内容
4.@data(*解析数据的方法(txt/csv文件))
这里我们还可以使用txt文件来对测试数据进行装载,需要的时候就进行读取。
注:在.txt文件中,在首行要加上Data,第二行才开始写数据。
from selenium import webdriver
import unittest
import time
from ddt import ddt, unpack, data, file_data #这里是一些应用的标签
import sys, csv
from selenium.webdriver.common.by import By
def getCsv(file_name):
rows = []
path = sys.path[0]
print(path)
with open(path + '/mytxt/' + file_name, 'rt', encoding='utf-8') as f:
readers = csv.reader(f, delimiter=',', quotechar='|')
next(readers, None)
for row in readers:
temprows = []
for i in row:
temprows.append(i)
rows.append(temprows)
return rows
@ddt
class myUnittest4(unittest.TestCase):
def setUp(self) :
self.driver = webdriver.Firefox()
self.url = "https://www.baidu.com/index.php?tn=monline_3_dg"
self.driver.get(self.url)
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
self.driver.quit()
@data(*getCsv('test_baidu_data.txt'))
@unpack
def test_data(self, value, name):
driver = self.driver
driver.maximize_window()
driver.find_element_by_id("kw").send_keys(value)
driver.find_element(By.ID, "su").click()
time.sleep(5)
self.assertEqual(driver.title, name, msg="名称不同")
time.sleep(2)
if __name__ == '__main__':
unittest.main()
直接给代码,这里就说一下代码里面需要注意的地方吧
from selenium.webdriver.common.by import By:这种定位元素的方式可以常用,笔者之前用的 driver.find_element_by_id("kw").send_keys(value)这种的有些时候定位不到
readers = csv.reader(f, delimiter=',', quotechar='|'):f为读取的流,delimiter是以什么为分隔符,quotechar中的| 为换行符。
next(readers, None)开始读取该.txt文件的意思
然后还有就是自己的文件位置问题,这个根据控制台来改就行。