zoukankan      html  css  js  c++  java
  • Python 各种测试框架简介

    转载:https://blog.csdn.net/yockie/article/details/47415265

    一、doctest

    doctest 是一个 Python 发行版自带的标准模块。本篇将分别对使用 doctest 的两种方式——嵌入到源代码中和做成独立文件做基本介绍。

    1.doctest的概念模型

    在 Python 的官方文档中,对 doctest 的介绍是这样的:

    doctest 模块会搜索那些看起来像交互式会话的 Python 代码片段,然后尝试执行并验证结果

    即使从没接触过 doctest,我们也可以从这个名字中窥到一丝端倪。“它看起来就像代码里的文档字符串(docstring)一样” 如果你这么想的话,就已经对了一半了。

    doctest 的编写过程就仿佛你真的在一个交互式 shell(比如 idle)中导入了要测试的模块,然后开始一条条地测试模块里的函数一样。实际上有很多人也是这么做的,他们写好一个模块后,就在 shell 里挨个测试函数,最后把 shell 会话复制粘贴成 doctest 用例。

    2.嵌入源代码模式

    下面使用的例子是一个只有一个函数的模块,其中签入了两个 doctest 的测试用例。

    unnecessary_math.py:

    
    """
    这里也可以写
    """
    def multiply(a,b):
        """
        >>> multiply(2,3)
        6
        >>> multiply('baka~',3)
        'baka~baka~baka~'
        """
        return a*b
    
    if __name__ == '__main__':
        import doctest
        doctest.testmod(verbose=True)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    注意测试代码的位置,前面说过 doctest 的测试用例就像文档字符串一样,这句话的内涵在于:测试用例的位置必须放在整个模块文件的开头,或者紧接着对象声明语句的下一行。也就是可以被 _ doc _ 这个属性引用到的地方。并非像普通注释一样写在哪里都可以。另:verbose 参数用于控制是否输出详细信息,默认为 False,如果不写,那么运行时不会输出任何东西,除非测试 fail。

    示例的运行输出为:

    Trying: 
    multiply(2,3) 
    Expecting: 

    ok 
    Trying: 
    multiply(‘baka~’,3) 
    Expecting: 
    ‘baka~baka~baka~’ 
    ok 
    1 items had no tests: 
    main 
    1 items passed all tests: 
    2 tests in main.multiply 
    2 tests in 2 items. 
    2 passed and 0 failed. 
    Test passed.

    值得一提的是,如果将这个脚本保存为doctest.py,并且运行,你会得到以下结果:

    Traceback (most recent call last): 
    File “doctest.py”, line 62, in 
    doctest.testmod() 
    AttributeError: ‘module’ object has no attribute ‘testmod’

    原因是被重写了,把文件名改成doctest1.py(或其他名字)之后,需要删除之前的pyc文件。再运行即可。

    上例中启动测试的方式是在 _ main _ 函数里调用了 doctest.testmod() 函数。这对于纯容器型模块文件来说是一个好办法——正常使用时只做导入用,直接运行文件则进行测试。而对于 _ main _ 函数另有他用的情况,则还可以通过命令行来启动测试:

    $ python -m doctest unnecessary_math.py
    $ python -m doctest -v unnecessary_math.py
    • 1
    • 2

    这里-m 表示引用一个模块,-v 等价于 verbose=True。运行输出与上面基本一样。

    3.独立文件模式

    如果不想(或不能)把测试用例写进源代码里,则还可以使用一个独立的文本文件来保存测试用例。

    可选的一些解释性内容...
    
    >>> from test import multiply
    >>> multiply(2,3)
    6
    >>> multiply('baka~',3)
    'baka~baka~baka~'
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    几乎同样的格式。运行方法可以分为在 Python shell 里运行或者在系统 shell 里运行:

    >>> import doctest
    >>> doctest.testfile('example.txt')
    • 1
    • 2

    bash/cmd.exe:

    $ python -m doctest -v example.txt
    • 1

    【摘自:链接1

    二、unittest

    unittest 与 doctest 一样也是 Python 发行版自带的包。如果你听说过 PyUnit(OSC 开源项目页面中就有 PyUnit 的页面),那么这俩其实是同一个东西——PyUnit 是 unittest 的曾用名,因为 PyUnit 最早也是来源于 Kent 和 Erich 的 JUnit(xUnit 测试框架系列的 Java 版本)

    1.unittest 概览

    上一篇介绍的 doctest 不管是看起来还是用起来都显得十分简单,可以与源码写在一起,比较适合用作验证性的功能测试。而本篇的 unittest 从名字上看,它是一个单元测试框架;从官方文档的字数上看,它的能力应该比 doctest 强一些。

    使用 unittest 的标准流程为:

    1. 从 unittest.TestCase 派生一个子类 
    2. 在类中定义各种以 “test_” 打头的方法 
    3. 通过 unittest.main() 函数来启动测试

    unittest 的一个很有用的特性是 TestCase 的 setUp() 和 tearDown() 方法,它们提供了为测试进行准备和扫尾工作的功能,听起来就像上下文管理器一样。这种功能很适合用在测试对象需要复杂执行环境的情况下。

    2.举个例子

    这里依旧使用上篇中那个极简的例子:unnecessary_math.py 文件中有一个 multiply() 函数,功能与 * 操作符完全一样。

    test_um_test.py:

    import unittest
    from unnecessary_math import multiply
    
    class TestUM(unittest.TestCase):
        def setUp(self):
            pass
        def test_number_3_4(self):
            self.assertEqual(multiply(3,4),12)
        def test_string_a_3(self):
            self.assertEqual(multiply('a',3),'aaa')
    
    if __name__ == '__main__':
        unittest.main()
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这个例子里,我们使用了 assertEqual() 方法。unittest 中还有很多类似的 assert 方法,比如 NotEqual、Is(Not)None、True(False)、Is(Not)Instance 等针对变量值的校验方法;另外还有一些如 assertRaises()、assertRaisesRegex() 等针对异常、警告和 log 的检查方法;以及如 assertAlmostEqual() 等一些奇怪的方法。

    较详细的 assert 方法可以参考 unittest 的文档页面

    3.启动测试

    上例中的结尾处,我们定义了一个对 unittest.main() 的调用,因此这个脚本是可以直接运行的:

    $ python test_um_test.py
    ..
    --------------------------------------
    Ran 2 tests in 0.01s
    
    OK
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    同样 -v 参数是可选的,也可以在 unittest.main() 函数里直接指定:verbosity=1。

    4.Test Discovery

    这个分段标题我暂时没想到好的翻译方法,就先不翻了。

    Test Discovery 的作用是:假设你的项目文件夹里面四散分布着很多个测试文件。当你做回归测试的时候,一个一个地执行这些测试文件就太麻烦了。TestLoader.discover() 提供了一个可以在项目目录下自动搜索并运行测试文件的功能,并可以直接从命令行调用:

    $ cd project_directory
    $ python -m unittest discover

    discover 可用的参数有 4 个(-v -s -p -t),其中 -s 和 -t 都与路径有关,如上例中提前 cd 到项目路径的话这俩参数都可以无视;-v 喜闻乐见;-p 是 –pattern 的缩写,可用于匹配某一类文件名。

    5.测试环境

    当类里面定义了 setUp() 方法的时候,测试程序会在执行每条测试项前先调用此方法;同样地,在全部测试项执行完毕后,tearDown() 方法也会被调用。验证如下:

    import unittest
    
    class simple_test(unittest.TestCase):
        def setUp(self):
            self.foo = list(range(10))
    
        def test_1st(self):
            self.assertEqual(self.foo.pop(),9)
    
        def test_2nd(self):
            self.assertEqual(self.foo.pop(),9)
    
    if __name__ == '__main__':
        unittest.main()

    注意这里两次测试均对同一个实例属性 self.foo 进行了 pop() 调用,但测试结果均为 pass,即说明,test_1st 和 test_2nd 在调用前都分别调用了一次 setUp()。

    那如果我们想全程只调用一次 setUp/tearDown 该怎么办呢?就是用 setUpClass() 和 tearDownClass() 类方法啦。注意使用这两个方法的时候一定要用 @classmethod 装饰器装饰起来:

    import unittest
    
    class simple_test(unittest.TestCase):
        @classmethod
        def setUpClass(self):
            self.foo = list(range(10))
    
        def test_1st(self):
            self.assertEqual(self.foo.pop(),9)
    
        def test_2nd(self):
            self.assertEqual(self.foo.pop(),8)
    
    if __name__ == '__main__':
        unittest.main()

    这个例子里我们使用了一个类级别的 setUpClass() 类方法,并修改了第二次 pop() 调用的预期返回值。运行结果显示依然是全部通过,即说明这次在全部测试项被调用前只调用了一次 setUpClass()。

    再往上一级,我们希望在整个文件级别上只调用一次 setUp/tearDown,这时候就要用 setUpModule() 和 tearDownModule() 这两个函数了,注意是函数,与 TestCase 类同级:

    import unittest
    
    def setUpModule():
        pass
    
    class simple_test(inittest.TestCase):
        ...

    一般 assert*() 方法如果抛出了未被捕获的异常,那么这条测试用例会被记为 fail,测试继续进行。但如果异常发生在 setUp() 里,就会认为测试程序自身存在错误,后面的测试用例和 tearDown() 都不会再执行。即,tearDown() 仅在 setUp() 成功执行的情况下才会执行,并一定会被执行。

    最后,这两个方法的默认实现都是什么都不做(只有一句 pass),所以覆盖的时候直接写新内容就可以了,不必再调用父类的此方法。

    三、nose

    本篇将介绍的 nose 不再是 Python 官方发行版的标准包,但它与 unittest 有着千丝万缕的联系。比如 nose 的口号就是:

    扩展 unittest,nose 让测试更简单。

    1.简单在哪

    自古(1970)以来,任何标榜“更简单”的工具所使用的手段基本都是隐藏细节,nose 也不例外。nose 不使用特定的格式、不需要一个类容器,甚至不需要 import nose ~(这也就意味着它在写测试用例时不需要使用额外的 api)

    前两篇中一直使用的 unnecessary_math.py 的 nose 版测试用例是这样子的:

    from unnecessary_math import multiply
    
    def test_numbers():
        assert multiply(3,4)==12
    
    def test_strings():
        assert multiply('a',3)=='aaa'

    看上去完全就是一个普通的模块文件嘛,甚至连 main 函数都不用。这里唯一需要一点“讲究”的语法在于:测试用例的命名仍需以 test_ 开头。

    2.运行 nose

    nose 在安装的时候也向你 Python 根目录下的 Scripts 文件夹内添加了一个名为 nosetests 的可执行文件,这个可执行文件就是用来执行测试的命令;当然你也仍可以使用 -m 参数来调用 nose 模块:

    $ nosetests test.py
    $ python -m nose test.py
    ··
    ------------------------------------------------
    Ran 2 tests in 0.001s
    
    OK

    另外非常棒的一点是,nosetests 兼容对 doctest 和 unittest 测试脚本的解析运行。如果你认为 nose 比那两个都好用的话,完全可以放弃 doctest 和 unittest 的使用。

    3.测试环境

    由于扩展自 unittest,nose 也支持类似于 setUp() setUpClass() setUpModule() 的测试环境创建方式,只不过函数命名规则最好改一改,我们可以使用更符合 Python 规范的命名规则。另外因为 nose 支持上例中所展示的函数式测试用例,所以还有一种为单个函数创建运行环境的装饰器可用。下面我们将使用一个例子来展示这四种功能的用法。

    test.py:

    from nose import with_setup 
    from unnecessary_math import multiply
    
    def setup_module(module):
        print('setup_module 函数执行于一切开始之前')
    
    def setup_deco():
        print('setup_deco 将用于 with_setup')
    
    def teardown_deco():
        print('teardown_deco 也将用于 with_setup')
    
    @with_setup(setup_deco,teardown_deco)
    def test_2b_decorated():
        assert multiply(3,4)==12
    
    class TestUM():
        def setup(self):
            print('setup 方法执行于本类中每条用例之前')
    
        @classmethod
        def setup_class(cls):
            print('setup_class 类方法执行于本类中任何用例开始之前,且仅执行一次')
    
        def test_strings(self):
            assert multiply('a',3)=='aaa'

    运行 $ nosetests -v test.py 结果如下:

    test.TestUM.test_strings … ok 
    test.test_2b_decorated … ok


    Ran 2 tests in 0.002s

    OK

    我们的 print() 函数一点东西都没打出来,如果你想看的话,给 nosetests 添加一个 -s 参数就可以了。

    4.Test Discovery

    nose 的 discovery 规则为:

    1.长得像测试用例,那就是测试用例。路径、模块(文件)、类、函数的名字如果能和 testMatch 正则表达式匹配上,那就会被认为是一个用例。另外所有 unittest.TestCase 的子类也都会被当做测试用例。(这里的 testMatch 可能是个环境变量之类的东西,我没有去查,因为反正你只要以 test_ 开头的格式来命名就可以保证能被发现)

    2.如果一个文件夹既长得不像测试用例,又不是一个包(路径下没有 init.py)的话,那么 nose 就会略过对这个路径的检查。

    3.但只要一个文件夹是一个包,那么 nose 就一定会去检查这个路径。

    4.显式避免某个对象被当做测试用例的方法为:给其或其容器添加一个 _ test _ 属性,并且运算结果不为 True。并不需要直接指定为 False,只要 bool( _ test _ ) == False 即可。另外,这个属性的添加方式比较特别,确认自己已经掌握使用方法前最好都试试。例如在类里面需要添加为类属性而非实例属性(即不能写在 _ init _(self) 里),否则不起作用。这里因为只是简介,就不挨个试了。(官方文档里就没解释清楚…)

    调用 discovery 的语法为,cd 到目录后直接调用 $ nosetests,后面不跟具体的文件名。另外这种方法其实对 unittest 也适用。

    四、pytest

    pytest 有时也被称为 py.test,是因为它使用的执行命令是 $ py.test。本文中我们使用 pytest 指代这个测试框架,py.test特指运行命令。

    1.较于 nose

    这里没有使用像前三篇一样(简介-举例-discovery-环境)式的分段展开,是因为 pytest 与 nose 的基本用法极其相似。因此只做一个比较就好了。他俩的区别仅在于

    1.调用测试的命令不同,pytest 用的是 $ py.test 
    2.创建测试环境(setup/teardown)的 api 不同

    下面使用一个例子说明 pytest 的 setup/teardown 使用方式。

    some_test.py:

    import pytest
    
    @pytest.fixture(scope='function')
    def setup_function(request):
        def teardown_function():
            print("teardown_function called.")
        request.addfinalizer(teardown_function)
        print('setup_function called.')
    
    @pytest.fixture(scope='module')
    def setup_module(request):
        def teardown_module():
            print("teardown_module called.")
        request.addfinalizer(teardown_module)
        print('setup_module called.')
    
    
    def test_1(setup_function):
        print('Test_1 called.')
    
    def test_2(setup_module):
        print('Test_2 called.')
    
    def test_3(setup_module):
        print('Test_3 called.')

    pytest 创建测试环境(fixture)的方式如上例所示,通过显式指定 scope=” 参数来选择需要使用的 pytest.fixture 装饰器。即一个 fixture 函数的类型从你定义它的时候就确定了,这与使用 @nose.with_setup() 十分不同。对于 scope=’function’ 的 fixture 函数,它就是会在测试用例的前后分别调用 setup/teardown。测试用例的参数如 def test_1(setup_function) 只负责引用具体的对象,它并不关心对方的作用域是函数级的还是模块级的。

    有效的 scope 参数限于:’function’,’module’,’class’,’session’,默认为 function。

    运行上例:$ py.test some_test.py -s。 -s 用于显示 print() 函数

    ============================= test session starts =============================
    platform win32 -- Python 3.3.2 -- py-1.4.20 -- pytest-2.5.2
    collected 3 items
    
    test.py setup_function called.
    Test_1 called.
    .teardown_function called.
    setup_module called.
    Test_2 called.
    .Test_3 called.
    .teardown_module called.
    
    
    ========================== 3 passed in 0.02 seconds ==========================

    这里需要注意的地方是:setup_module 被调用的位置。

    2.pytest 与 nose 二选一

    首先,单是从不需要使用特定类模板的角度上,nose 和 pytest 就较于 unittest 好出太多了。doctest 比较奇葩我们在这里不比。因此对于 “选一个自己喜欢的测试框架来用” 的问题,就变成了 nose 和 pytest 二选一的问题。

    pythontesting.net 的作者非常喜欢 pytest,并表示

    “如果你挑不出 pytest 的毛病,就用这个吧”。

    于是下面我们就来挑挑 pytest 的毛病:

    它的 setup/teardown 语法与 unittest 的兼容性不如 nose 高,实现方式也不如 nose 直观 
    第一条足矣 
    毕竟 unittest 还是 Python 自带的单元测试框架,肯定有很多怕麻烦的人在用,所以与其语法保持一定兼容性能避免很多麻烦。即使 pytest 在命令行中有彩色输出让我很喜欢,但这还是不如第一条重要。

    实际上,PyPI 中 nose 的下载量也是 pytest 的 8 倍多。

    所以假如再继续写某一个框架的详解的话,大概我会选 nose 吧。

  • 相关阅读:
    codeforces水题100道 第十一题 Codeforces Round #143 (Div. 2) A. Team (brute force)
    codeforces水题100道 第十题 Codeforces Round #277 (Div. 2) A. Calculating Function (math)
    codeforces水题100道 第九题 Codeforces Beta Round #63 (Div. 2) Young Physicist (math)
    codeforces水题100道 第八题 Codeforces Round #274 (Div. 2) A. Expression (math)
    vs2015
    强制IE浏览器或WebBrowser控件使用指定版本显示网页
    一个基于jquery的智能提示控件intellSeach.js
    WebBrowser中html元素如何触发winform事件
    ASP.NET用DataSet导出到Excel
    ASP.NET Excel数据导入数据库
  • 原文地址:https://www.cnblogs.com/ceshi2016/p/9110738.html
Copyright © 2011-2022 走看看