Python 各种测试框架简介 转载:https://blog.csdn.net/yockie/article/details/47415265 一、doctest 二、unittest 三、nose 四、pytest

一、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 吧。