【原创】一文读懂单元测试框架(python+unittest)
一、单元测试的定义:
-
什么是单元测试:
单元测试是指,对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作,这里的最小可测试单元通常是指函数或者类,一般是开发来做的,按照测试阶段来分,就是单元测试、集成测试、系统测试以及验收测试。 -
为什么要做单元测试?
1)单元测试之后,才是集成测试,单个单个的功能模块测试通过之后,才能把单个功能模块集成起来做集成测试,为了从底层发现bug,单元测试时可以减少合成后出现的问题。2)越早发现bug越好,这样可以早点发现问题,不然问题累计到后面,很可能会因为一个做错了而导致整个模块甚至更大范围的推倒重来,对于时间和经费来说,是非常浪费的!
2)对于测试来说,单元测试就是为了执行用例,输入测试数据–》输出测试结果
二、unittest框架及原理:
-
unittest是python自带的一套测试框架,最核心的四个概念:
① test case
就是我们的测试用例,unittest中提供了一个基本类TestCase,可以用来创建新的测试用例,一个TestCase的实例就是一个测试用例;unittest中测试用例方法都是以test开头的,且执行顺序会按照方法名的ASCII值排序。编写测试用例前,我们需要建一个测试类继承unittest里面的TestCase类,继承这个类之后我们才是真正的使用unittest框架去写测试用例,编写测试用例的步骤如下:
-
导入unittest模块
-
创建一个测试类,并继承unittest.TestCase()
-
定义测试方法,方法名必须以test_开头
-
调用unittest.main()方法来运行测试用例,unittest.main()方法会搜索该模块下所有以test开头的测试用例方法,并自动执行
② test fixture
测试夹具,用于测试用例环境的搭建和销毁。即用例测试前准备环境的搭建(SetUp前置条件),测试后环境的还原(TearDown后置条件),比如测试前需要登录获取token等就是测试用例需要的环境,运行完后执行下一个用例前需要还原环境,以免影响下一条用例的测试结果。
unittest的测试夹具有两种使用方式,一种是以测试方法为维度的setUp()和tearDown(),一种是以测试类为维度的setUpClass()和tearDownClass()。
③test suite
测试套件,用来把需要一起执行的测试用例集中放到一块执行,相当于一个篮子。我们可以使用TestLoader来加载测试用例到测试套件中。
unittest.TestSuite()类来表示一个测试用例集,把需要执行的用例类或模块存到一起,常用的方法如下: - unittest.TestSuite() addTest():添加单个测试用例方法 addTest([..]):添加多个测试用例方法,方法名存在一个列表 - unittest.TestLoader() loadTestsFromTestCase(测试类名):添加一个测试类 loadTestsFromModule(模块名):添加一个模块 - discover(测试用例的所在目录):指定目录去加载,会自动寻找这个目录下所有符合命名规则的测试用例
④test runner
用来执行测试用例的,并返回测试用例的执行结果。它还可以用图形或者文本接口,把返回的测试结果更形象的展现出来,如:HTMLTestRunner。
三、test case 测试用例
下面以加减乘除计算功能为例,这个 “myMath.py” 就是加减乘除计算功能的代码,没有前端界面,功能比较简单,只是方便用于演示,直接导入就可以使用。
#定义一个类,实现加减乘除算法
#对这个类中的加减乘除方法进行单元测试
class myMath():
def jia(self,a,b):
return a+b
def jian(self,a,b):
return a-b
def cheng(self,a,b):
return a*b
def chu(self,a,b):
return a/b
下面是编写测试用例 “test_mymath.py” 例子:(test_mymath.py和myMath.py在同一路径下)
#导入unittest
import unittest
#导入被测试模块
import myMath
class test_mymath(unittest.TestCase):
#一种注解,在python,java中使用这种方法给方法指定了特定的含义
@classmethod
def setUpClass(cls):
print("我是setUpClass方法")
# cls.mm = myMath.myMath()
#所有用例都需要的参数可以放在setUp()中,setUpClass()也可以,最好放在setUp里面
@classmethod
def tearDownClass(cls):
print("我是tearDownClass方法")
#方法名不能改,self参数不能少
def setUp(self):
print("我是setUp方法")
self.mm = myMath.myMath()
# #方法名不能改,self参数不能少
def tearDown(self):
print("我是teardown方法")
#用这个注销对象,释放资源
self.mm=None
#必须是test开头的方法,第一个就是测试用例
#测试加法
def test_jia(self):
print("我是第一条测试用例!")
#mm=myMath.myMath()
actualValue=self.mm.jia(10,12)
exceptedValue=22
#该方法判断给定的两个参数是否相等
self.assertEqual(actualValue,exceptedValue,"预期结果和实际不相等")
#actualValue=myMath.jia(a,b)
def test_jian(self):
print("我是第二条测试用例!")
#创建一个对象
#mm=myMath.myMath()
actualValue=self.mm.jian(12,10)
exceptedValue=2
#该方法判断给定的两个参数是否相等
self.assertEqual(actualValue,exceptedValue,"预期结果和实际不相等")
#actualValue=myMath.jia(a,b)
def test_cheng(self):
print("我是第三条测试用例!")
#创建一个对象
#mm=myMath.myMath()
try:
actualValue=self.mm.cheng(5,8)
exceptedValue=40
#该方法判断给定的两个参数是否相等
self.assertEqual(actualValue,exceptedValue,"预期结果和实际不相等")
except Exception as e:
print("该方法有问题",e)
#actualValue=myMath.jia(a,b)
def test_chu(self):
print("我是第四条测试用例!")
#创建一个对象
#mm=myMath.myMath()
actualValue=self.mm.chu(8,2)
exceptedValue=4
#该方法判断给定的两个参数是否相等
self.assertEqual(actualValue,exceptedValue,"预期结果和实际不相等")
#actualValue=myMath.jia(a,b)
if __name__ == '__main__':
#调用执行单元测试类,通过主方法main执行
#这个方法可以执行全部的测试用例,全部的测试类中全部的测试用例
#如果想执行加法运算的单元测试用例
#unittest.main()
suitt=unittest.TestSuite()
#测试集合对象中有一个方法,addTest-->追加单个测试用例到集合中
#格式:类名(用例名称),要求掌握
#suitt.addTest(test_mymath("test_jia"))
#如果测试用例的数量比较大,使用testsuite自带的方法用例到集合,很麻烦
# 测试集合对象中有一个方法,addTests-->追加多个测试用例到集合中
# list1=[test_mymath("test_chu"),test_mymath("test_cheng")]
# suitt.addTests(list1)
#可以用unittest中提供的testloader模块,提供把多个测试用例加载到测试集合的方法
#创建一个testloader对象
loader=unittest.TestLoader()
#通过添加一个模块名,将其中的用例直接加载到测试集合
#suitt=loader.loadTestsFromModule(myMath)
#loaderTestFromName:通过添加一个模块名、类名、测试用例名,将其中的用例直接加载到测试集合
# suitt = loader.loadTestsFromName("test_mymath.test_mymath.test_chu")
#使用defaultTestLoader对象的discover方法加载用例,是将指定路径下的所有符合匹配规则(pattern)的文件中的单元测试用例一次性加载
#第一个参数指定一个目录:这个目录下可以有单元测试用例的文件
#“unit*.py指的是以unit开头,以.py结尾的文件
#因为verydows*.py中是线性脚本的,直接运行
#加载测试集合并运行的测试用例有几条?0条
#case_path = '../unitest02/test/'
discover=unittest.defaultTestLoader.discover(r'./', pattern="test_mymath.py")
print(suitt.countTestCases())
#执行discover
runner = unittest.TextTestRunner()
runner.run(discover)
#测试集合中有run方法,直接运行即可
#执行loader or addTest
# re=unittest.TestResult()
# suitt.run(re)
# print(re.__dict__)
运行以上测试用例,即可。
上面代码有些注释我没有删除掉,特别是执行测试用例的主函数,我将对主函数的一些注释进行解释:
- unittest.main()
调用执行单元测试类,通过主方法main执行,这个方法可以执行全部的测试用例,全部的测试类中全部的测试用例
缺点:无法控制用例的执行顺序 - suitt=unittest.TestSuite()
所以测试套件test suite 的出现是为了解决主函数main的问题。test suite类似于一个测试用例的集合,可使用test suite中的两个方法addTest()、addTests(),向该集合中追加被测试的用例(使用方法可参考代码注释) - loader=unittest.TestLoader()
用loader方法loadTestsFromName()将指定的测试用例加载到测试集合中,并返回给测试集合(使用方法可参考代码注释)
loadTestsFromName()的参数:
1)“模块名”
2)“模块名.类名”
3)“模块名.类名.测试用例方法名” - discover方法
使用defaultTestLoader对象的discover方法加载用例,是将指定路径下的所有符合匹配规则(pattern)的文件中的单元测试用例一次性加载。参数 r’./’ 指名模块路径为当前路径,pattern中的“unit*.py指的是以unit开头,以.py结尾的文件
四、unittest断言
如果断言失败即不通过就会抛出一个AssertionError断言错误,成功则标识为通过,以上几种方式都有一个共同点,就是都有一个msg参数(表中只列了2个,其实都有),默认是None,即msg = None,如果指定msg参数的值,则将该信息作为失败的错误信息返回。
下面贴出测试以上所有断言方法的assert.py代码:(test_mymath.py、assert.py和myMath.py在同一路径下)
import unittest
import myMath
class test_mymath(unittest.TestCase):
def test_assert(self):
a=12
b="12"
c="12"
d=13
e=None
f=4
g=[1,2,3,4]
mm=myMath.myMath()
result_true=a<d
result_false=a>d
self.assertEqual(b,c,"两个参数不相等")
self.assertNotEqual(a,b,"两个参数相等")
self.assertTrue(result_true)
self.assertFalse(result_false)
self.assertIs(c,b,"a is not b")
self.assertIsNot(a,b,"a is b")
self.assertIsNone(e)
self.assertIsNotNone(mm)
self.assertIn(f,g)
self.assertNotIn(a,g)
print(id(e))
self.assertIsInstance(a,int)
if __name__ == '__main__':
unittest.main()
参考:https://www.cnblogs.com/miki-peng/p/12501341.html