前几天,听公司某老板谈编程经验。他讲了“测试驱动开发”,觉得我测试能力薄弱。因此,写完这篇文章,我希望得到一个测试入门。这段时间,笔者也体会到了测试的价值。总之,学会测试可以让你的开发更有效率。本文将从以下两个方面进行介绍:TestwithCoverageMockTestwithCoverage测试覆盖率通常用于衡量测试的充分性和完整性。从广义上讲,主要有两大类:面向项目的需求覆盖和更多的技术代码覆盖。对于开发者,我们更关注代码覆盖率。代码覆盖率是指至少执行一次的词条数占总词条数的百分比。如果条目数是一条语句,则对应代码行覆盖率;如果条目数是函数,则对应函数覆盖率;如果条目数是路径,则对应路径覆盖率,依此类推。统计代码覆盖率的根本目的是找出潜在缺失的测试用例,并有针对性地进行补充。同时,它还可以识别代码中那些因需求变更等原因而导致的废弃代码。一般我们希望代码覆盖率越高越好。代码覆盖率越高,越说明你的测试用例设计的充分和完整,但是随着代码覆盖率的增加,测试的成本也会增加。在Python中,coverage模块帮助我们实现了代码行覆盖,我们可以很方便的使用它来完成测试代码行覆盖。我们通过一个例子来介绍覆盖模块的使用。首先,我们有脚本func_add.py,它实现了添加功能。代码如下:#-*-coding:utf-8-*-defadd(a,b):ifisinstance(a,str)andisinstance(b,str)):returna+'+'+belifisinstance(a,list)andisinstance(b,list):返回a+belifisinstance(a,(int,float))andisinstance(b,(int,float)):returna+belse:returnNone在add函数中,实现了四种情况下的加法,分别是字符串、列表、属性值和其他情况。接下来,我们使用unittest模块进行单元测试。代码脚本(test_func_add.py)如下:importunittestfromfunc_addimportaddclassTest_Add(unittest.TestCase):defsetUp(self):passdeftest_add_case1(self):a="Hello"b="World"res=add(a,b)打印(res)self.assertEqual(res,"Hello+World")deftest_add_case2(self):a=1b=2res=add(a,b)print(res)self.assertEqual(res,3)deftest_add_case3(self):a=[1,2]b=[3]res=add(a,b)print(res)self.assertEqual(res,[1,2,3])deftest_add_case4(self):a=2b="3"res=add(a,b)print(None)self.assertEqual(res,None)if__name__=='__main__':#一些用例测试#构造一个容器来存储我们的测试用例suite=unittest.TestSuite()#在类中添加测试用例suite.addTest(Test_Add('test_add_case1'))suite.addTest(Test_Add('test_add_case2'))#suite.addTest(Test_Add('test_add_case3'))#suite.addTest(Test_Add('test_add_case4'))run=unittest.TextTestRunner()run.run(suite)在这个测试中,我们只测试了前两个用例,即字符串和数字的相加测试输入在命令行中coverageruntest_func_add.py命令运行测试脚本,输出结果如下:Hello+World.3.---------------------------------------------------------------------跑2在0.000sOK中测试,然后输入命令coveragehtml以生成有关代码行覆盖率的报告。将生成htmlcov文件夹。打开其中的index.html文件,可以看到本次执行的覆盖率,如下图:我们点击func_add.py查看add函数的测试,如下图:可以看到,单元测试脚本test_func_add.py的前两个测试用例只覆盖了add函数左边的绿色部分,没有测试红色部分,代码行覆盖率为75%。所以,还有两种情况没有覆盖,说明我们单元测试中的测试用例还不够。在test_func_add.py中,我们删除了main函数中的注释并添加了最后两个测试用例。这时我们运行上面覆盖模块的命令,重新生成htmlcov后,func_add.py的代码行覆盖率如下图所示:可以看到添加测试用例后,代码行覆盖率我们调用的add函数的覆盖率是100%,所有的代码都被覆盖了。MockMock这个词在英文中有模拟的意思,所以我们可以猜到这个库的主要功能就是模拟一些东西。准确的说,Mock是Python中用来支持单元测试的一个库。它的主要功能是将指定的Python对象替换为模拟对象,实现模拟对象的行为。在Python3中,mock是一个辅助单元测试的模块。它允许您用模拟对象替换系统的某些部分,并对它们的使用方式做出断言。实际生产中的项目非常复杂。在对它们进行单元测试时,会遇到以下问题:接口依赖外部接口调用测试环境非常复杂单元测试应该只对当前单元进行,所有内部或外部依赖都应该是稳定的并且已经在别处测试过。使用mock可以模拟和替代外部依赖组件的实现,让单元测试可以只关注当前单元功能。我们用一个简单的例子来说明mock模块的使用。首先我们有脚本mock_multipy.py,主要作用是Operator类中的multipy函数,这里我们可以假设函数没有实现,但是有这么一个函数,代码如下:#-*-coding:utf-8-*-#mock_multipy.pyclassOperator():defmultipy(self,a,b):pass虽然我们还没有实现multipy函数,但是还是想测试一下这个函数的作用。这时候,我们就可以使用mock模块的Mock类来实现。测试脚本(mock_example.py)代码如下:#-*-coding:utf-8-*-fromunittestimportmockimportunittestfrommock_multipyimportOperator#testOperatorclassclassTestCount(unittest.TestCase):deftest_add(self):op=Operator()#使用Mock类,我们假设返回结果为15op.multipy=mock.Mock(return_value=15)#调用multipy函数,入参为4,5,实际结果=op.multipy(4,5)#声明返回结果是否为15self.assertEqual(result,15)if__name__=='__main__':unittest.main()我们来解释一下上面的代码。op.multipy=mock.Mock(return_value=15)使用Mock类模拟调用Operator类中的multipy()函数,return_value定义了multipy()方法的返回值。result=op.multipy(4,5)结果值调用multipy()函数,入参为4,5,但实际上并没有调用,最后通过assertEqual()方法断言返回的结果是否为15的预期结果。输出结果如下:Ran1testin0.002sOK通过Mock类,即使没有实现multipy函数,我们仍然可以通过想象函数执行的结果进行测试,这样如果有后续依赖multipy函数的函数,不影响后续代码的测试。利用Mock模块中的patch功能,我们可以将上面的测试脚本代码简化为:测试用例):@patch("mock_multipy.Operator.multipy")deftest_case1(self,tmp):tmp.return_value=15result=Operator().multipy(4,5)self.assertEqual(15,result)if__name__=='__main__':unittest.main()patch()装饰器使得在模块测试中模拟类或对象变得容易。您指定的对象将在测试期间替换为模拟(或其他对象)并在测试结束时恢复。那如果我们后面再实现multipy这个功能,还能测试吗?修改mock_multipy.py脚本,代码如下:#-*-coding:utf-8-*-#mock_multipy.pyclassOperator():defmultipy(self,a,b):returna*b此时,我们再运行mock_example.py脚本,测试依然通过,这是因为multipy函数返回的结果仍然是我们mock之后返回的值,并没有调用真正的Operator类中的multipy函数。我们修改mock_example.py脚本如下:#-*-coding:utf-8-*-fromunittestimportmockimportunittestfrommock_multipyimportOperator#testOperatorclassclassTestCount(unittest.TestCase):deftest_add(self):op=Operator()#使用Mock类,添加side_effect参数op.multipy=mock.Mock(return_value=15,side_effect=op.multipy)#调用multipy函数,入参为4,5,实际调用结果=op.multipy(4,5)#声明返回结果是否为15self.assertEqual(result,15)if__name__=='__main__':unittest.main()side_effect参数和return_value参数相反。它为模拟分配了一个替代结果,覆盖了return_value。简单地说,模拟工厂调用将返回side_effect值,而不是return_value。因此,如果在Operator类中将side_effect参数设置为multipy函数,则return_value函数将失效。运行修改后的测试脚本,测试结果如下:Ran1testin0.004sFAILED(failures=1)15!=20Expected:20Actual:15可以发现multipy函数返回的值为20,不是等于期望值15,这是side_effect函数的结果,返回的结果调用了Operator类中的multipy函数,所以返回值为20。将self.assertEqual(result,15)中的15改为20),运行测试结果如下:Ran1testin0.002sOK
