Python中参数化测试的实现思路解析

本文想针对测试中一种很常见的测试场景,即参数化测试,继续聊聊关于测试的话题,并尝试将这几个测试框架串联起来,做一个横向的比对,加深理解。
1、什么是参数化测试?
对于普通测试来说,一个测试方法只需要运行一遍,而参数化测试对于一个测试方法,可能需要传入一系列参数,然后进行多次测试。
比如,我们要测试某个系统的登录功能,就可能要分别传入不同的用户名与密码,进行测试:使用包含非法字符的用户名、使用未注册的用户名、使用超长的用户名、使用错误的密码、使用合理的数据等等。
参数化测试是一种“数据驱动测试”(data-driven test),在同一个方法上测试不同的参数,以覆盖所有可能的预期分支的结果。它的测试数据可以与测试行为分离,被放入文件、数据库或者外部介质中,再由测试程序读取。
2、参数化测试的实现思路?
通常而言,一个测试方法就是一个最小的测试单元,其功能应该尽量地原子化和单一化。
先来看看两种实现参数化测试的思路:一种是写一个测试方法,在其内部对所有测试参数进行遍历;另一种是在测试方法之外写遍历参数的逻辑,然后依次调用该测试方法。
这两种思路都能达到测试目的,在简单业务中,没有毛病。然而,实际上它们都只有一个测试单元,在统计测试用例数情况,或者生成测试报告的时候,并不乐观。可扩展性也是个问题。
那么,现有的测试框架是如何解决这个问题的呢?
它们都借助了装饰器,主要的思路是:利用原测试方法(例如 test()),来生成多个新的测试方法(例如 test1()、test2()……),并将参数依次赋值给它们。
由于测试框架们通常把一个测试单元统计为一个“test”,所以这种“由一生多”的思路相比前面的两种思路,在统计测试结果时,就具有很大的优势。
3、参数化测试的使用方法?
python 标准库中的unittest自身不支持参数化测试,为了解决这个问题,有人专门开发了两个库:一个是ddt,一个是parameterize。
ddt 正好是“data-driven tests”(数据驱动测试)的缩写。典型用法:
import unittest from ddt import ddt,data,unpack @ddt class mytest(unittest.testcase): @data((3, 1), (-1, 0), (1.2, 1.0)) @unpack def test_values(self, first, second): self.asserttrue(first > second) unittest.main(verbosity=2) 运行的结果如下:
test_values_1__3__1_ (__main__.mytest) ... ok test_values_2___1__0_ (__main__.mytest) ... fail test_values_3__1_2__1_0_ (__main__.mytest) ... ok ================================================== fail: test_values_2___1__0_ (__main__.mytest) -------------------------------------------------- traceback (most recent call last): file c:/python36/lib/site-packages/ddt.py, line 145, in wrapper return func(self, *args, **kwargs) file c:/users/pythoncat/pycharmprojects/study/testparam.py, line 9, in test_values self.asserttrue(first > second) assertionerror: false is not true ---------------------------------------------- ran 3 tests in 0.001s failed (failures=1) 结果显示有 3 个 tests,并详细展示了运行状态以及断言失败的信息。
需要注意的是,这 3 个 test 分别有一个名字,名字中还携带了其参数的信息,而原来的 test_values 方法则不见了,已经被一拆为三。
在上述例子中,ddt 库使用了三个装饰器(@ddt、@data、@unpack),实在是很丑陋。下面看看相对更好用的 parameterized 库:
import unittest from parameterized import parameterized class mytest(unittest.testcase): @parameterized.expand([(3,1), (-1,0), (1.5,1.0)]) def test_values(self, first, second): self.asserttrue(first > second) unittest.main(verbosity=2) 测试结果如下:
test_values_0 (__main__.mytest) ... ok test_values_1 (__main__.mytest) ... fail test_values_2 (__main__.mytest) ... ok ========================================= fail: test_values_1 (__main__.mytest) ----------------------------------------- traceback (most recent call last): file c:/python36/lib/site-packages/parameterized/parameterized.py, line 518, in standalone_func return func(*(a + p.args), **p.kwargs) file c:/users/pythoncat/pycharmprojects/study/testparam.py, line 7, in test_values self.asserttrue(first > second) assertionerror: false is not true ---------------------------------------- ran 3 tests in 0.000s failed (failures=1) 这个库只用了一个装饰器 @parameterized.expand,写法上可就清爽多了。
同样提醒下,原来的测试方法已经消失了,取而代之的是三个新的测试方法,只是新方法的命名规则与 ddt 的例子不同罢了。
介绍完 unittest,接着看已经死翘翘了的nose以及新生的nose2。nose 系框架是带了插件(plugins)的 unittest,以上的用法是相通的。
另外,nose2 中还提供了自带的参数化实现:
import unittest from nose2.tools import params @params(1, 2, 3) def test_nums(num): assert num < 4 class test(unittest.testcase): @params((1, 2), (2, 3), (4, 5)) def test_less_than(self, a, b): assert a second) 测试结果如下:
==================== test session starts ==================== platform win32 -- python 3.6.1, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 rootdir: c:/users/pythoncat/pycharmprojects/study collected 3 items testparam.py .f testparam.py:3 (test_values[-1-0]) first = -1, second = 0 @pytest.mark.parametrize(first,second, [(3,1), (-1,0), (1.5,1.0)]) def test_values(first, second): > assert(first > second) e assert -1 > 0 testparam.py:6: assertionerror . [100%] ========================= failures ========================== _________________________ test_values[-1-0] _________________________ first = -1, second = 0 @pytest.mark.parametrize(first,second, [(3,1), (-1,0), (1.5,1.0)]) def test_values(first, second): > assert(first > second) e assert -1 > 0 testparam.py:6: assertionerror ===================== 1 failed, 2 passed in 0.08s ===================== process finished with exit code 0 依然要提醒大伙注意,pytest 也做到了由一变三,然而我们却看不到有新命名的方法的信息。这是否意味着它并没有产生新的测试方法呢?或者仅仅是把新方法的信息隐藏起来了?
4、最后小结
上文中介绍了参数化测试的概念、实现思路,以及在三个主流的 python 测试框架中的使用方法。我只用了最简单的例子,为的是快速科普(言多必失)。
但是,这个话题其实还没有结束。对于我们提到的几个能实现参数化的库,抛去写法上大同小异的区别,它们在具体代码层面上,又会有什么样的差异呢?
具体来说,它们是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数绑定起来的呢?在实现中,需要解决哪些棘手的问题?
在分析一些源码的时候,我发现这个话题还挺有意思,所以准备另外写一篇文章。那么,本文就到此为止了,谢谢阅读。
作者简介: 豌豆花下猫,生于广东毕业于武大,现为苏漂程序员,有一些极客思维,也有一些人文情怀,有一些温度,还有一些态度。


工业自动化热门技术之控制系统网络化
或将引发新的骨传导耳机大战:Nank南卡首款入门级骨传导耳机发布,仅售249!
物联网在医疗保健行业解决的七个痛点
基于MX27处理器和CH7024芯片实现机载视频输出接口设计
跑步戴哪款无线耳机好,最好的跑步耳机推荐
Python中参数化测试的实现思路解析
欧瑞博、艾拉物联、绿米三家无主灯方案对比,谁更胜一筹?
什么是工业机器人末端执行器
基于ITER导体横截面样品和核磁兼容式,加快芯屏器合等新兴产业发展
微软Windows 10将内置眼动追踪技术 到底什么是眼动追踪技术?
MSO5204数字示波器概述及特点
为什么在新设计中使用 LPWAN 模块?
全数字式微波对射探测器是什么,它都有哪些优势
电动汽车充电桩的组成部分
巴西已允许华为公司继续在该国推进其5G网络建设
集成MEMS微振镜的3D相机,让智能装备看得“清”
Profinet协议转换为CanOpen协议来连接CanOpen伺服驱动器的应用
佳能利用人工智能进行面部识别的“笑脸识别”门禁管理系统
黑客在不到20分钟内,就能偷走你的指纹
BUCK降压电源布线注意事项