Python中如何实现参数化测试?

玩技站长
玩技站长
管理员, Keymaster
11056
文章
0
粉丝
测试资讯评论285字数 1621阅读5分24秒阅读模式
摘要本文想针对测试中一种很常见的测试场景,即参数化测试,继续聊聊关于测试的话题,并尝试将这几个测试框架串联起来,做一个横向的比对,加深理解。

Python中如何实现参数化测试?插图

之前,我曾转过一个单元测试框架系列的文章,里面介绍了 unittest、nose/nose2 与 pytest 这三个最受人欢迎的 Python 测试框架。文章源自玩技e族-https://www.playezu.com/194351.html

本文想针对测试中一种很常见的测试场景,即参数化测试,继续聊聊关于测试的话题,并尝试将这几个测试框架串联起来,做一个横向的比对,加深理解。文章源自玩技e族-https://www.playezu.com/194351.html

1、什么是参数化测试?文章源自玩技e族-https://www.playezu.com/194351.html

对于普通测试来说,一个测试方法只需要运行一遍,而参数化测试对于一个测试方法,可能需要传入一系列参数,然后进行多次测试。文章源自玩技e族-https://www.playezu.com/194351.html

比如,我们要测试某个系统的登录功能,就可能要分别传入不同的用户名与密码,进行测试:使用包含非法字符的用户名、使用未注册的用户名、使用超长的用户名、使用错误的密码、使用合理的数据等等。文章源自玩技e族-https://www.playezu.com/194351.html

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

2、参数化测试的实现思路?文章源自玩技e族-https://www.playezu.com/194351.html

通常而言,一个测试方法就是一个最小的测试单元,其功能应该尽量地原子化和单一化。文章源自玩技e族-https://www.playezu.com/194351.html

先来看看两种实现参数化测试的思路:一种是写一个测试方法,在其内部对所有测试参数进行遍历;另一种是在测试方法之外写遍历参数的逻辑,然后依次调用该测试方法。文章源自玩技e族-https://www.playezu.com/194351.html

这两种思路都能达到测试目的,在简单业务中,没有毛病。然而,实际上它们都只有一个测试单元,在统计测试用例数情况,或者生成测试报告的时候,并不乐观。可扩展性也是个问题。文章源自玩技e族-https://www.playezu.com/194351.html

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

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

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

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

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

ddt 正好是“Data-Driven Tests”(数据驱动测试)的缩写。典型用法:

  1. importunittest
  2. fromddtimportddt,data,unpack
  3. @ddt
  4. classMyTest(unittest.TestCase):
  5. @data((3,1),(-1,0),(1.2,1.0))
  6. @unpack
  7. deftest_values(self,first,second):
  8. self.assertTrue(first>second)
  9. unittest.main(verbosity=2)

运行的结果如下:

  1. test_values_1__3__1_(__main__.MyTest)...ok
  2. test_values_2___1__0_(__main__.MyTest)...FAIL
  3. test_values_3__1_2__1_0_(__main__.MyTest)...ok
  4. ==================================================
  5. FAIL:test_values_2___1__0_(__main__.MyTest)
  6. --------------------------------------------------
  7. Traceback(mostrecentcalllast):
  8. File"C:\Python36\lib\site-packages\ddt.py",line145,inwrapper
  9. returnfunc(self,*args,**kwargs)
  10. File"C:/Users/pythoncat/PycharmProjects/study/testparam.py",line9,intest_values
  11. self.assertTrue(first>second)
  12. AssertionError:Falseisnottrue
  13. ----------------------------------------------
  14. Ran3testsin0.001s
  15. FAILED(failures=1)

结果显示有 3 个 tests,并详细展示了运行状态以及断言失败的信息。

需要注意的是,这 3 个 test 分别有一个名字,名字中还携带了其参数的信息,而原来的 test_values 方法则不见了,已经被一拆为三。

在上述例子中,ddt 库使用了三个装饰器(@ddt、@data、@unpack),实在是很丑陋。下面看看相对更好用的 parameterized 库:

  1. importunittest
  2. fromparameterizedimportparameterized
  3. classMyTest(unittest.TestCase):
  4. @parameterized.expand([(3,1),(-1,0),(1.5,1.0)])
  5. deftest_values(self,first,second):
  6. self.assertTrue(first>second)
  7. unittest.main(verbosity=2)

测试结果如下:

  1. test_values_0(__main__.MyTest)...ok
  2. test_values_1(__main__.MyTest)...FAIL
  3. test_values_2(__main__.MyTest)...ok
  4. =========================================
  5. FAIL:test_values_1(__main__.MyTest)
  6. -----------------------------------------
  7. Traceback(mostrecentcalllast):
  8. File"C:\Python36\lib\site-packages\parameterized\parameterized.py",line518,instandalone_func
  9. returnfunc(*(a+p.args),**p.kwargs)
  10. File"C:/Users/pythoncat/PycharmProjects/study/testparam.py",line7,intest_values
  11. self.assertTrue(first>second)
  12. AssertionError:Falseisnottrue
  13. ----------------------------------------
  14. Ran3testsin0.000s
  15. FAILED(failures=1)

这个库只用了一个装饰器 @parameterized.expand,写法上可就清爽多了。

同样提醒下,原来的测试方法已经消失了,取而代之的是三个新的测试方法,只是新方法的命名规则与 ddt 的例子不同罢了。

介绍完 unittest,接着看已经死翘翘了的nose 以及新生的nose2 。nose 系框架是带了插件(plugins)的 unittest,以上的用法是相通的。

另外,nose2 中还提供了自带的参数化实现:

  1. importunittest
  2. fromnose2.toolsimportparams
  3. @params(1,2,3)
  4. deftest_nums(num):
  5. assertnum<4
  6. classTest(unittest.TestCase):
  7. @params((1,2),(2,3),(4,5))
  8. deftest_less_than(self,a,b):
  9. asserta<b

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

  1. importpytest
  2. @pytest.mark.parametrize("first,second",[(3,1),(-1,0),(1.5,1.0)])
  3. deftest_values(first,second):
  4. assert(first>second)

测试结果如下:

  1. ====================testsessionstarts====================
  2. platformwin32--Python3.6.1,pytest-5.3.1,py-1.8.0,pluggy-0.13.1
  3. rootdir:C:\Users\pythoncat\PycharmProjects\studycollected3items
  4. testparam.py.F
  5. testparam.py:3(test_values[-1-0])
  6. first=-1,second=0
  7. @pytest.mark.parametrize("first,second",[(3,1),(-1,0),(1.5,1.0)])
  8. deftest_values(first,second):
  9. >assert(first>second)
  10. Eassert-1>0
  11. testparam.py:6:AssertionError
  12. .[100%]
  13. =========================FAILURES==========================
  14. _________________________test_values[-1-0]_________________________
  15. first=-1,second=0
  16. @pytest.mark.parametrize("first,second",[(3,1),(-1,0),(1.5,1.0)])
  17. deftest_values(first,second):
  18. >assert(first>second)
  19. Eassert-1>0
  20. testparam.py:6:AssertionError
  21. =====================1failed,2passedin0.08s=====================
  22. Processfinishedwithexitcode0

依然要提醒大伙注意,pytest 也做到了由一变三,然而我们却看不到有新命名的方法的信息。这是否意味着它并没有产生新的测试方法呢?或者仅仅是把新方法的信息隐藏起来了?

4、最后小结

上文中介绍了参数化测试的概念、实现思路,以及在三个主流的 Python 测试框架中的使用方法。我只用了最简单的例子,为的是快速科普(言多必失)。

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

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

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

 
匿名

发表评论

匿名网友
确定

拖动滑块以完成验证