Unittest单元测试框架
unittest是什么
unittest是python内置的单元测试框架,具备编写用例、组织用例、执行用例、输出报告等自动化框架的条件。
单元测试框架的优点
一般来说不用单元测试框架也能编写单元测试,因为单元测试本身就是通过一段代码去验证另一段代码,所以不用单元测试框架也能编写单元测试。只是使用框架时会有更多的优点
1、提供用例组织与执行:
当测试用例达到成百上千条时,就产生了扩展性与维护性等问题,此时就需要考虑用例的规范与组织问题了。单元测试框架便能很好的解决这个问题
2、提供丰富的比较方法:
不论是功能测试还是单元测试,在用例完成之后都需要将实际结果与预期结果进行比较(断言),从而断定用例是否执行通过。单元测试框架一般会提供丰富的断言方法。例如:相等\不相等,包含\不包含,True\False的断言方法等
3、提供丰富的日志:
当测试用例执行失败时能抛出清晰的失败原因,当所有用例执行完成之后能提供丰富的执行结果。例如,总执行时间、失败用例数、成功用例数等
认识单元测试
例1:下面为一段需要测试的代码
class Count():
def __init__(self,x,y):
self.x = int(x)
self.y = int(y)
def Add(self,c):
sum = self.x + self.y + c
return sum
def Subtract(self):
subtract = self.x - self.y
return subtract
def Product(self):
product = self.x * self.y
return product
def Division(self):
division = self.x / self.y
return division
if __name__ == "__main__":
count = Count(6,3)
print(count.Add(2))
print(count.Subtract())
print(count.Product())
print(count.Division())
"""
11
3
18
2.0
"""
例1_1:不使用框架编写的单元测试
#使用非Unittest框架进行单元测试
#from Module.Unittest_Module.Add_count import Count
#调用其他模块中的函数:from 模块路径.模块名.类名 import 函数名
from Module.Unittest_Module import Add_count
#调用其他模块中的函数:from 模块路径.模块名 import 类名
class Test_Add_Count():
def test_add(self):
try:
count = Add_count.Count(2, 3)
#count = Count(2, 3)
Add = count.Add(2)#不管怎么调用,调用方法时都是实例名.方法名()
#assert (Count.Add == 7), "Integer addition result error"
#assert (Add_count.Count.Add == 7), "Integer addition result error"
assert (Add == 7),"Integer addition result error"
except AssertionError as Error_msg:
print(Error_msg)
else:
print("Test pass")
test_case = Test_Add_Count()
test_case.test_add()
"""
#assert比较相等的结果时
Test pass
#assert比较不相等的结果时
7
Integer addition result error
"""
备注:
1、上面代码的执行过程为:调用Module.Unittest_Module下面的Add_count模块下的Count类,然后在test_add方法中调用Count类的Add()方法并传入对应的参数进行对应的加法运算,并通过assert()方法判断Add()的返回值是否是7,如果不相等则抛出自定的异常,相等则打印"Test pass"
2、从上面的代码可以看出,测试程序的写法没有一定的规范,不同的人会有不同的写法,不统一的代码维护起来十分麻烦。因此为了让单元测试代码更容易维护和编写,最好的方式就是遵循一定的规范来写。这就是单元测试框架诞生的初衷。
3、从上面的代码注释等可以看出,在调用其他模块中的类时,有不同的调用方法,总结出来就是一句话:最后要确定函数的具体位置(模块路径.模块名.类名.函数名)。不同的调用方法在实例化类时有点不一致,但最终调用类中的方法都是一样的(实例名.方法名( ))
例1_2:
#使用单元测试框架(Unittest)来写
from Module.Unittest_Module import Add_count
import unittest
class TestAddCount(unittest.TestCase):
def setUp(self):
print("测试开始")
def test_add_case(self):
count = Add_count.Count(6, 3)
self.assertEqual(count.Add(2),11) #测试函数的返回值
def test_subtract_case(self):
count = Add_count.Count(6, 3)
self.assertEqual(count.Subtract(),3)
def test_product_case(self):
count = Add_count.Count(6, 3)
self.assertEqual(count.Product(),18)
def test_division_case(self):
count = Add_count.Count(6, 3)
self.assertEqual(count.Division(),2)
def tearDown(self):
print("测试结束")
if __name__=="__main__":
unittest.main()
"""
#测试通过时
测试开始
测试结束
OK
#测试未通过时
测试开始
测试结束
3 != 2.0
Expected :2.0
Actual :3
"""
备注:分析上面的代码
1、首先引入unittest模块,创建TestAddCount测试类并继承unittest的TestCase类,我们可以把TestCase类看成是对特定类进行测试的集合
2、setUp()方法用于测试用例执行前的初始化工作,这里只是简单地打印"测试开始"。tearDown()方法与setUP()方法相呼应,用于测试用例执行之后的善后工作,这里打印"测试结束"信息
3、在test_add_case()等方法中,首先是调用Count类并传入要计算的参数(实例化类),然后在调用类中对应的方法(Add()等)得到对应的返回值。这里就不在使用繁琐的异常处理了,而是直接调用unittest框架所提供的assertEqual()方法对Add()的返回值进行断言,判断两者是否相等。assertEqual()方法由Testcase类集成而来
4、unittest提供了全局main()方法,使用它可以方便地将一个单元测试模块变成可以直接运行的测试脚本。main()方法使用TestLoader类来搜索所有包含在该模块中以"test"命名开头的测试方法,并自动执行它们(因此:所有的测试函数以test开头)
5、在unittest.main()中加 verbosity 参数可以控制输出的错误报告的详细程度,默认是 1,如果设为 0,则不输出每一用例的执行结果,即没有上面的结果中的第1行;如果设为 2,则输出详细的执行结果
单元测试重要的概念
使用unittest前需要了解该框架的五个概念,即test case,test suite,testLoader,test runner,test fixture
1、Test Case:
一个TestCase的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程。包括测试前准备环境的搭建(SetUP)、实现测试过程的代码(run),以及测试后环境的还原(tearDown)。单元测试(Unittest)的本质也就在这里,一个测试用例就是一个完整的测试单元,通过运行这个测试单元,可以对某一个功能进行验证
2、Test Suite:
一个功能的验证往往需要多个测试用例,可以把多个测试用例集合在一起执行,这就产生了测试套件TestSuite的概念。TestSuite用来组装单个测试用例。可以通过addTest加载TestCase到TestSuite中,从而返回一个TestSuite实例。而且TestSuite也可以嵌套TestSuite
3、Test Runner:
测试的执行也是单元测试中非常重要的一个概念,一般单元测试框架中都会提供丰富的执行策略和执行结果。在Unittest单元测试框架中,通过TextTestRunner类提供的run()方法来执行test suite/test case。test runner可以使用图形界面、文本界面,或返回一个特殊的值等方式来表示测试执行的结果
4、Test Fixture:
对一个测试用例环境的搭建和销毁,就是一个Fixture,通过覆盖TestCase的setUP()和tearDown()方法来实现。这有什么用呢?比如说在这个测试用例中需要访问数据库,那么可以在setUP()中建立数据库连接来进行初始化,在tearDown()中清除数据库产生的数据,然后关闭连接
注:tearDown()方法的过程很重要,要为下一个TestCase留下一个干净的环境
例2:
from Module.Unittest_Module import Add_count
import unittest
class TestAddCount(unittest.TestCase):
def setUp(self):
print("测试开始")
def test_add_case(self):
count = Add_count.Count(6, 3)
print("测试加法")
self.assertEqual(count.Add(2),11)
def test_subtract_case(self):
count = Add_count.Count(6, 3)
print("测试减法")
self.assertEqual(count.Subtract(),3)
def test_product_case(self):
count = Add_count.Count(6, 3)
print("测试乘法")
self.assertEqual(count.Product(),18)
def test_division_case(self):
count = Add_count.Count(6, 3)
print("测试除法")
self.assertEqual(count.Division(),2)
def tearDown(self):
print("测试结束")
if __name__=="__main__":
#构造测试集
suite = unittest.TestSuite()
suite.addTest(TestAddCount("test_division_case"))
#执行测试
runner = unittest.TextTestRunner()
runner.run(suite)
"""
测试开始
测试加法
测试结束
测试开始
测试除法
测试结束
测试开始
测试乘法
测试结束
测试开始
测试减法
测试结束
Ran 4 tests in 0.008s
OK
"""
注:
1、这样写出来后若是直接执行的话,还是会执行全部的用例;因此还需要专门设置下才能实现执行测试集(TestSuite)中的用例,设置步骤如下图
2、从上面的例子中我们去掉了main()方法,采用构造测试集的方法来加载与运行测试用例,实现了有选择的执行测试用例。当然,也可以通过注释的方法来注释掉其余不需要执行的用例,但是这种做法并不优雅
3、上面代码的执行过程:首先调用unittest框架的TestSuite()类来创建测试套件(实例化),通过它提供的addTest()方法来添加测试用例(test_division_case)。接着调用unittest框架的TextTestRunner()类(实例化),通过它下面的run()方法来运行suite所组装的测试用例
unitest的工作原理
通过unittest类调用分析,可将框架的工作流程概况如下:
编写TestCase,由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,最后将运行的结果保存在TextTestResult中
备注:
执行测试用例后,并把测试结果写入到TXT中
例3:
from Module.Unittest_Module import Add_count
import unittest
class TestAddCount(unittest.TestCase):
def setUp(self):
print("测试开始")
def test_add_case(self):
count = Add_count.Count(6, 3)
print("测试加法")
self.assertEqual(count.Add(2),11)
def test_subtract_case(self):
count = Add_count.Count(6, 3)
print("测试减法")
self.assertEqual(count.Subtract(),3)
def test_product_case(self):
count = Add_count.Count(6, 3)
print("测试乘法")
self.assertEqual(count.Product(),18)
def test_division_case(self):
count = Add_count.Count(6, 3)
print("测试除法")
self.assertEqual(count.Division(),2)
def tearDown(self):
print("测试结束")
if __name__=="__main__":
suite = unittest.TestSuite()#实例化一个测试集
suite.addTest(TestAddCount("test_division_case"))
with open('F:\Pycharm_project\\UnittestTextReport.txt',"w") as UnittestTextReport:
runner = unittest.TextTestRunner(stream=UnittestTextReport, verbosity=2)
runner.run(suite)
"""
测试开始
测试除法
测试结束
"""
拓展
unitest框架中的setUp()方法
unittese框架的中setUp()方法给我的感觉就是定义一些实例变量(与__init__方法一样),且里面的变量整个类都可以调用(在同一个类中可以通过self.实例变量名来调用)
例4:
import unittest
#待试函数
def Subtract(x,y):
subtract = x - y
return subtract
class TestSuite(unittest.TestCase):
def setUp(self):
self.a = 1
def test_case_one(self):
count = Subtract(6, 3) - self.a #通过self.属性名来调用setUp()方法中的变量
self.assertEqual(count, 2)
def tearDown(self):
print("测试结束")
if __name__ == "__main__":
unittest.main()
"""
测试结束
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
"""
例4_1:
import unittest
from AOC.Common.dbConnect import DB
from AOC.Common.dataDeal import DealData
import time
class TestSuite(unittest.TestCase):
def setUp(self):
self.db = DB()#实例化一个类并定义为实例变量
self.db_conn, self.db_cursor = self.db.ConnectDB(db_info="这里是数据库信息")
def test_case_one(self):
insert_sql = "这是SQL语句"
try:
self.db_cursor.execute(insert_sql)
self.db_conn.commit()
print("插入航班数据成功")
except BaseException:
self.db_conn.rollback()
raise ("!!!插入失败!!!,错误原因为")
def tearDown(self):
self.db_cursor.close()
self.db_conn.close()
print("测试结束")
if __name__ == "__main__":
unittest.main()
"""
插入航班数据成功
测试结束
----------------------------------------------------------------------
Ran 1 test in 1.051s
OK
"""
注:
1、这个例子就比较贴近实际场景了:在执行测试用例前连接数据库,在执行测试用例后管理数据库连接
2、另外这个例子中需要注意的就是在Test Case和tearDown方法中都用到了setUp()方法中定义的变量,这就是前面说的:
⑴setUp()方法感觉就是定义一些实例变量(与__init__方法一样),在同一个类中可以通过self.实例变量名来调用
同一个类中引用其他方法中的变量
1、构造方法__init__用于定义类的实例变量(每个实例独有的属性)
2、调用实例属性:
⑴在类中调用实例属性时为:self.属性名
⑵在类外调用实例属性时为:实例名.属性名
例4:
class Count():
def __init__(self,x,y):
self.x = int(x)
self.y = int(y)
def Add(self,c):
self.c = c
sum = self.x + self.y + self.c
return sum
def Subtract(self):
subtract = self.x - self.y - self.c
return subtract
count = Count(2,3)
print(count.Add(2))
print(count.Subtract())
"""
7
-3
"""
1、在Count()类中的Add()方法中有一个自己的变量C,且将其定义成了一个类似于实例变量的变量(实际不是实例变量),且在其他方法中调用了该变量(Subtract()方法中)
⑴这种写法时,给我的感觉就是:此时变量C就相当于是个全局变量(实例变量),在这个类中所有的方法都可以调用,但是就必须先执行,定义该变量的方法(先调用Add()方法),如果不先调用Add方法的话,就会报错,
⑵所以这样写没有实际意思,还不如不将其赋值给变量(就直接c)。所以方法中有自己的变量的话,还是只给自己用就好了,不要想着给别的方法中,即使在同一个类中
2、__init__方法中的变量跟上面说的变量c不是一个东西,__init__中的变量是实例变量,用于初始化(相当于一个全局变量),在这个类中或类外的任何方法都可以调用
3、unittese框架的中setUp()方法给我的感觉就跟__init__方法一样,都是用于初始化,且里面的变量整个类都可以调用(具体例子看下一篇)
例4_1:
class Count():
def __init__(self,x,y):
self.x = int(x)
self.y = int(y)
def Add(self,c):
self.c = c
sum = self.x + self.y + self.c
return sum
def Subtract(self):
subtract = self.x - self.y - self.c
return subtract
count = Count(2,3)
#print(count.Add(2))
print(count.Subtract())
"""
AttributeError: 'Count' object has no attribute 'c'
"""
例4_2:
class Count():
def __init__(self,x,y):
self.x = int(x)
self.y = int(y)
def Add(self,c):
#self.c = c
sum = self.x + self.y + c
return sum
def Subtract(self):
subtract = self.x - self.y - c
return subtract
count = Count(2,3)
print(count.Add(2))
print(count.Subtract())
"""
NameError: name 'c' is not defined
"""
例4_3:
正确的写法:完全把变量c当做私有变量只能在定义的方法中调用,其他方法都不能调用
class Count():
def __init__(self,x,y):
self.x = int(x)
self.y = int(y)
def Add(self,c):
sum = self.x + self.y + c
return sum
def Subtract(self):
subtract = self.x - self.y
return subtract
count = Count(2,3)
print(count.Add(2))
print(count.Subtract())
"""
7
-1
"""