之前翻到单元测试框架系列文章,介绍了unittest、nose/nose2和pytest这三个最流行的Python测试框架。本文想围绕测试中一个非常常见的测试场景,即参数化测试,继续聊测试这个话题,并尝试将这些测试框架串联起来,进行横向比较,加深理解。1.什么是参数测试?对于普通的测试,一个测试方法只需要运行一次,但是对于参数化测试,可能需要向一个测试方法中传入一系列的参数,然后进行多次测试。比如我们要测试某个系统的登录功能,可能需要传入不同的用户名和密码进行测试:使用含有非法字符的用户名,使用未注册的用户名,使用超长用户名,使用错误密码,使用敏感数据等。参数化测试是一种“数据驱动测试”,它在同一方法上测试不同的参数以覆盖所有可能的预期分支的结果。它的测试数据可以从测试行为中分离出来,放入文件、数据库或外部介质中,然后由测试程序读取。2、参数化测试的实现思路是什么?一般来说,一个测试方法是最小的测试单元,它的功能应该尽可能的原子化和简单化。下面我们来看两种实现参数化测试的方法:一种是写一个测试方法,遍历里面所有的测试参数;另一种是在测试方法外写遍历参数的逻辑,然后调用测试方法。这两种思路都可以达到测试的目的。在简单的业务中,它没有错。但实际上,它们都只有一个测试单元,无论是统计测试用例数量还是生成测试报告,都不容乐观。可扩展性也是一个问题。那么,现有的测试框架是如何解决这个问题的呢?它们都使用装饰器,主要思想是:利用原有的测试方法(如test())生成多个新的测试方法(如test1()、test2()...),并为它们分配参数。由于测试框架通常将一个测试单元算作一个“测试”,所以这种“不止一个生命周期”的思路相比前两种思路在统计测试结果上有很大的优势。3.如何使用参数化测试?Python标准库中的unittest本身不支持参数化测试。为了解决这个问题,有人专门开发了两个库:一个是ddt,一个是参数化的。ddt恰好是“数据驱动测试”的首字母缩写词。典型用法:importunittestfromddtimportddt,data,unpack@ddtclassMyTest(unittest.TestCase):@data((3,1),(-1,0),(1.2,1.0))@unpackdeftest_values(self,first,second):self。assertTrue(first>second)unittest.main(verbosity=2)结果如下:test_values_1__3__1_(__main__.MyTest)...oktest_values_2___1__0_(__main__.MyTest)...FAILtest_values_3__1_2__1_0_(__main__.MyTest)...ok====================================================失败:test_values_2___1__0_(__main__.MyTest)----------------------------------------------回溯(mostrecentcalllast):文件“C:\Python36\lib\site-packages\ddt.py”,line145,inwrapperreturnfunc(self,*args,**kwargs)File“C:/Users/pythoncat/PycharmProjects/study/testparam.py",line9,intest_valuesself.assertTrue(first>second)AssertionError:Falseisnottrue-----------------------------------------------Ran3testsin0.001sFAILED(failures=1)结果显示有3个测试,并详细显示了运行状态和断言失败信息。需要注意的是,这三个测试每个都有一个名字,名字中还携带了其参数的信息,而原来的test_values方法没有了,被拆分成了三个。在上面的例子中,ddt库使用了三个装饰器(@ddt、@data、@unpack),实在是太丑了。再来看看相对较好的参数化库:importunittestfromparameterizedimportparameterizedclassMyTest(unittest.TestCase):@parameterized.expand([(3,1),(-1,0),(1.5,1.0)])deftest_values(self,first,second):self.assertTrue(first>second)unittest.main(verbosity=2)测试结果如下:test_values_0(__main__.MyTest)...oktest_values_1(__main__.MyTest)...FAILtest_values_2(__main__.MyTest)...好吧============================================失败:test_values_1(__main__.MyTest)--------------------------------------Traceback(mostrecentcallast):File"C:\Python36\lib\site-packages\parameterized\parameterized.py",line518,instandalone_funcreturnfunc(*(a+p.args),**p.kwargs)File"C:/Users/pythoncat/PycharmProjects/study/testparam.py",line7,intest_valuesself.assertTrue(first>second)AssertionError:Falseisnottrue----------------------------------------Ran3testsin0.000sFAILED(failures=1)这个库只用了一个装饰器@parameterized.expand,就muchc写作更精简。提醒一下,原来的测试方法已经消失了,取而代之的是三个新的测试方法,但是新方法的命名规则与ddt例子不同。介绍完unittest,我们再看看deadnose和newnose2。基于nose的框架是unittestwithplugins,上面的用法类似。此外,nose2还提供了自己的参数化实现:importunittestfromnose2.toolsimportparams@params(1,2,3)deftest_nums(num):assertnum<4classTest(unittest.TestCase):@params((1,2),(2,3),(4,5))deftest_less_than(self,a,b):assertasecond)测试结果如下:=====================testsessionstarts=====================platformwin32--Python3.6.1,pytest-5.3.1,py-1.8。0,pluggy-0.13.1rootdir:C:\Users\pythoncat\PycharmProjects\studycollected3itemstestparam.py.Ftestparam.py:3(test_values[-1-0])first=-1,second=0@pytest.mark.parametrize(“第一,第二”,[(3,1),(-1,0),(1.5,1.0)])deftest_values(第一,第二):>断言(第一>第二)Eassert-1>0testparam.py:6:AssertionError.[100%]===========================失败============================____________________________test_values[-1-0]__________________________冷杉st=-1,second=0@pytest.mark.parametrize("first,second",[(3,1),(-1,0),(1.5,1.0)])deftest_values(first,second):>assert(first>second)Eassert-1>0testparam.py:6:AssertionError======================1failed,2passedin0.08s======================Processfinishedwithexitcode0还是要提醒大家,pytest也从一变成了三,但是我们看不到新命名方法的信息。这是否意味着它没有生成新的测试方法?或者它只是隐藏了有关新方法的信息?4.最后总结以上介绍了三种主流Python测试框架中参数化测试的概念、实现思路和使用方法。为了快速科学起见,我只使用了最简单的例子(更多的话会丢失)。然而,这个话题还没有结束。对于我们提到的几个可以实现参数化的库,抛开写法上的不同,在具体的代码层面会有什么样的不同呢?具体来说,他们是如何将一个方法变成多个方法,并为每个方法绑定相应的参数呢?在实施中,需要解决哪些棘手问题?在分析一些源码的时候,觉得这个话题挺有意思的,准备另写一篇。好了,本文就这些了,感谢阅读。
