V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
chinesehuazhou
V2EX  ›  Python

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

  •  
  •   chinesehuazhou · 2019-12-09 22:25:03 +08:00 · 3145 次点击
    这是一个创建于 1814 天前的主题,其中的信息可能已经有所发展或是发生改变。

    之前,我曾转过一个单元测试框架系列的文章,里面介绍了 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 测试框架中的使用方法。我只用了最简单的例子,为的是快速科普(言多必失)。

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

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

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

    2 条回复    2019-12-10 22:33:18 +08:00
    frostming
        1
    frostming  
       2019-12-10 21:28:54 +08:00
    许久不用,nose 已经死了?

    单测唯一推荐框架 pytest,我几乎不用别的了,hypothesis 听说过没用过
    chinesehuazhou
        2
    chinesehuazhou  
    OP
       2019-12-10 22:33:18 +08:00
    @frostming nose 官方已弃坑,搞了个 nose2。不过,还是比不过 pytest
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1877 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 16:30 · PVG 00:30 · LAX 08:30 · JFK 11:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.