Python 中如何实现参数化测试的方法示例


Posted in Python onDecember 10, 2019

之前,我曾转过一个单元测试框架系列的文章,里面介绍了 unittest、nose/nose2 与 pytest 这三个最受人欢迎的 Python 测试框架。

本文想针对测试中一种很常见的测试场景,即参数化测试,继续聊聊关于测试的话题,并尝试将这几个测试框架串联起来,做一个横向的比对,加深理解。

1、什么是参数化测试?

对于普通测试来说,一个测试方法只需要运行一遍,而参数化测试对于一个测试方法,可能需要传入一系列参数,然后进行多次测试。

比如,我们要测试某个系统的登录功能,就可能要分别传入不同的用户名与密码,进行测试:使用包含非法字符的用户名、使用未注册的用户名、使用超长的用户名、使用错误的密码、使用合理的数据等等。

参数化测试是一种“数据驱动测试”(Data-Driven Test),在同一个方法上测试不同的参数,以覆盖所有可能的预期分支的结果。它的测试数据可以与测试行为分离,被放入文件、数据库或者外部介质中,再由测试程序读取。

2、参数化测试的实现思路?

通常而言,一个测试方法就是一个最小的测试单元,其功能应该尽量地原子化和单一化。

先来看看两种实现参数化测试的思路:一种是写一个测试方法,在其内部对所有测试参数进行遍历;另一种是在测试方法之外写遍历参数的逻辑,然后依次调用该测试方法。

这两种思路都能达到测试目的,在简单业务中,没有毛病。然而,实际上它们都只有一个测试单元,在统计测试用例数情况,或者生成测试报告的时候,并不乐观。可扩展性也是个问题。

那么,现有的测试框架是如何解决这个问题的呢?

它们都借助了装饰器,主要的思路是:利用原测试方法(例如 test()),来生成多个新的测试方法(例如 test1()、test2()……),并将参数依次赋值给它们。

由于测试框架们通常把一个测试单元统计为一个“test”,所以这种“由一生多”的思路相比前面的两种思路,在统计测试结果时,就具有很大的优势。

3、参数化测试的使用方法?

Python 标准库中的unittest 自身不支持参数化测试,为了解决这个问题,有人专门开发了两个库:一个是ddt ,一个是parameterized 。

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 < b

最后,再来看下 pytest 框架,它这样实现参数化测试:

import pytest

@pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)])
def test_values(first, second):
  assert(first > 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 测试框架中的使用方法。我只用了最简单的例子,为的是快速科普(言多必失)。

但是,这个话题其实还没有结束。对于我们提到的几个能实现参数化的库,抛去写法上大同小异的区别,它们在具体代码层面上,又会有什么样的差异呢?

具体来说,它们是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数绑定起来的呢?在实现中,需要解决哪些棘手的问题?

在分析一些源码的时候,我发现这个话题还挺有意思,所以准备另外写一篇文章。那么,本文就到此为止了,谢谢阅读。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
利用Python进行异常值分析实例代码
Dec 07 Python
numpy.std() 计算矩阵标准差的方法
Jul 11 Python
Python和Java的语法对比分析语法简洁上python的确完美胜出
May 10 Python
PyQt5 QListWidget选择多项并返回的实例
Jun 17 Python
Python中的支持向量机SVM的使用(附实例代码)
Jun 26 Python
opencv+python实现均值滤波
Feb 19 Python
Python面向对象中类(class)的简单理解与用法分析
Feb 21 Python
让Django的BooleanField支持字符串形式的输入方式
May 20 Python
Expected conditions模块使用方法汇总代码解析
Aug 13 Python
python 如何调用 dubbo 接口
Sep 24 Python
分享一枚pycharm激活码适用所有pycharm版本我的pycharm2020.2.3激活成功
Nov 20 Python
Pytorch 中net.train 和 net.eval的使用说明
May 22 Python
利用python读取YUV文件 转RGB 8bit/10bit通用
Dec 09 #Python
YUV转为jpg图像的实现
Dec 09 #Python
Pandas+Matplotlib 箱式图异常值分析示例
Dec 09 #Python
Python箱型图处理离群点的例子
Dec 09 #Python
Python实现非正太分布的异常值检测方式
Dec 09 #Python
python 实现检验33品种数据是否是正态分布
Dec 09 #Python
Python远程开发环境部署与调试过程图解
Dec 09 #Python
You might like
php 中的str_replace 函数总结
2007/04/27 PHP
PHP中数组合并的两种方法及区别介绍
2012/09/14 PHP
php Imagick获取图片RGB颜色值
2014/07/28 PHP
Yii分页用法实例详解
2014/12/04 PHP
PHP也能干大事 随机函数
2015/04/14 PHP
PHP中的一些常用函数收集
2015/05/26 PHP
php算法实例分享
2015/07/14 PHP
PHP排序算法之基数排序(Radix Sort)实例详解
2018/04/21 PHP
在JavaScript中遭遇级联表达式陷阱
2007/03/08 Javascript
Javascript中call的两种用法实例
2013/12/13 Javascript
使用JS获取当前地理位置方法汇总
2014/12/18 Javascript
NodeJS学习笔记之(Url,QueryString,Path)模块
2015/01/13 NodeJs
vue从使用到源码实现教程详解
2016/09/19 Javascript
JS实现倒计时(天数、时、分、秒)
2016/11/16 Javascript
微信小程序购物商城系统开发系列-工具篇的介绍
2016/11/21 Javascript
javascript-解决mongoose数据查询的异步操作
2016/12/22 Javascript
Nodejs调用Dll模块的方法
2018/09/17 NodeJs
详解vuex状态管理模式
2018/11/01 Javascript
vue.js中ref和$refs的使用及示例讲解
2019/08/14 Javascript
Vue Elenent实现表格相同数据列合并
2020/11/30 Vue.js
Python面向对象编程中关于类和方法的学习笔记
2016/06/30 Python
关于python pyqt5安装失败问题的解决方法
2017/08/08 Python
使用python验证代理ip是否可用的实现方法
2018/07/25 Python
Python实现数据结构线性链表(单链表)算法示例
2019/05/04 Python
简单瞅瞅Python vars()内置函数的实现
2019/09/27 Python
解决torch.autograd.backward中的参数问题
2020/01/07 Python
腾讯技术类校园招聘笔试试题
2014/05/06 面试题
初三新学期计划书
2014/05/03 职场文书
乡镇党的群众路线教育实践活动个人对照检查材料
2014/09/23 职场文书
乡镇民主生活会发言材料
2014/10/20 职场文书
会计实训报告范文
2014/11/04 职场文书
承诺函格式模板
2015/01/21 职场文书
《黄山奇石》教学反思
2016/02/18 职场文书
Canvas跟随鼠标炫彩小球的实现
2021/04/11 Javascript
Python自动化测试PO模型封装过程详解
2021/06/22 Python
Java中PriorityQueue实现最小堆和最大堆的用法
2021/06/27 Java/Android