如果你已经读完我不测试部分,现在确信做测试驱动开发,那么祝贺!你已经学习了
测试驱动开发的基础知识,但在你能够有效地使用这种方法之前,还有一些你应该学习的
东西。
本节介绍开发人员在编写测试时遇到的一些问题以及解决这些问题的方法。同时还对
Python 社区中可用的流行测试运行器和工具做了快速回顾。
unittest 陷阱
unittest 模块在 Python 2.1 中引入,自那以后开发人员大量使用它。但是在社区中,
一些对单元测试的弱点和限制感到失望的人,创建了一些替代的测试框架。
这是一些常见批评。
● 框架有些臃肿,难以使用,原因如下。
○ 你必须在 TestCase 的子类中编写所有的测试。
○ 你必须在方法名前加上 test。
○ 我们鼓励使用 TestCase 中提供的断言方法,而不是单纯的断言语句,而现有
的方法可能不能覆盖所有的用例。
● 框架很难扩展,因为它需要大量的子类化其基类或者技巧,如装饰器。
● 测试固件有时很难组织,因为 setUp 和 tearDown 被绑定到 TestCase 级别,
虽然它们在每次测试只运行一次。换句话说,如果测试固件涉及许多测试模块,则
组织其创建和清理并不容易。
● 在 Python 软件上运行测试活动并不容易。默认测试运行器(python -m unittest)
确实提供了一些测试发现功能,但不提供足够的过滤功能。在实践中,必须编写额
外的脚本来收集测试,聚合它们,然后以适当的方式运行它们。
需要一种更轻量级的方法来编写测试,而不需要一个看起来太像其 Java 兄弟,JUnit
的刻板的框架。由于 Python 不需要使用 100%的基于类的环境,因此最好提供一个不基于
子类化的更 Python 化的测试框架。
常见的方法如下。
● 提供一种简单的方法来标记任何作为测试的函数或类。
● 通过插件系统扩展框架。
● 为所有测试级别提供完整的测试固件环境。整个活动,在测试级别上以模块进行分组。
● 为基于测试发现的测试运行器提供具有丰富的选项。
unittest 的替代品
一些第三方工具尝试通过以单元测试扩展的形式提供额外的特性来解决刚刚提到的问
题。Python 维基百科页面上提供了很多测试实用程序和框架列表(参考 https://wiki.python.org/
moin/ PythonTestingToolsTaxonomy),但只有两个项目特别受欢迎。
● nose:请参阅 http://nose.readthedocs.org。
● py.test:请参阅 http://pytest.org。
nose
nose 主要是一个具有强大的发现功能的测试运行器。它有丰富的选项,允许在 Python
应用程序中运行所有类型的测试活动。
它不是标准库的一部分,但在 PyPI 上可用,可以很容易地用 pip 安装:
pip install nose
测试运行器
安装 nose 后,在命令提示符下使用一个名为 nosetests 的新命令。
可以直接使用以下命令运行本章第一部分介绍的测试:
nosetests -v
test_true (test_primes.OtherTests) … ok
test_is_prime (test_primes.PrimesTests) … ok
builds the test suite. … ok
Ran 3 tests in 0.009s
OK
nose 通过递归地浏览当前目录进行测试发现并且自己构建一个测试套件。相比简单
的 python -m unittest 命令,上面的例子初看似乎没有任何改进。如果使用–help
开关运行此命令,差异将显而易见。你会注意到,nose 提供了几十个参数,允许你控制
测试发现和执行。
编写测试
nose 进一步运行所有类和函数,它们的名称匹配正则表达式(?:^|[b_.-])
[Tt]est),并且它们所在的模块也要匹配。大致上,所有以 test 开始并且位于与模式匹
配的模块中的可调用项也将被作为测试执行。
例如,下面这个 test_ok.py 模块将被识别并由 nose 运行:
$ more test_ok.py
def test_ok():
print(‘my test’)
$ nosetests -v
test_ok.test_ok … ok
Ran 1 test in 0.071s
OK
常规的 TestCase 类和 doctests 也会被执行。
最后,nose 提供了类似于 TestCase 方法的断言函数(assertion functions)。这些函数
遵循 PEP 8 命名约定,而不是使用 Java 单元测试中的约定(参考 http://nose.readthedocs.org/)。
编写测试固件
nose 支持 3 个级别的测试固件。
● 包级别:可以把 setup 和 teardown 函数添加到__init__.py 模块中,这是一
个包含所有测试模块的测试包。
● 模块级:测试模块可以有自己的 setup 和 teardown 函数。
● 测试级别:通过使用 with_setup 装饰器,使固件函数可调用。
例如,要在模块和测试级别设置测试固件,请使用以下代码:
def setup():
启动函数的代码,为整个模块加载
…
def teardown():
卸载函数的代码,为整个模块加载
…
def set_ok():
启动函数的代码,只为 test_ok 函数加载
…
@with_setup(set_ok)
def test_ok():
print(‘my test’)
与 setuptools 和插件系统集成
最后,nose 可以与 setuptools 平滑地集成,所以可以使用 test 命令进行测试
(python setup.py test)。此集成是通过向 setup.py 脚本中添加 test_suite 元数据
来完成的,如下所示:
setup(
#…
test_suite=‘nose.collector’,
)
nose 也使用 setuptool 的入口点机制为开发人员写 nose 插件。你可以覆盖或修改
工具的各个方面,从测试发现到格式化输出。
nose 小结
nose 是一个完整的测试工具,修复了很多 unittest 的问题。它也对测试使用隐式前缀
名称,这对于一些开发人员来说仍然是一个约束。虽然这个前缀可以自定义,但你仍然必
须遵循这个约定。
这个约定优于配置的语句并不糟糕,比 unittest 中需要的样板代码好多了。但是使
用显式装饰器,例如,这是一个摆脱测试前缀的很好的方法。
此外,使用插件扩展 nose 的功能使它变的非常灵活,开发人员可以定制工具以满足
他/她的需求。
如果你的测试工作流需要覆写很多 nose 参数,你可以在主目录或项目根目录中添
加.noserc 或 nose.cfg 文件来完成。它将为 nosetests 命令指定默认的选项集。例
如,一个好的做法是在测试运行期间自动查找 doctests。启用运行 doctests 的 nose
配置文件的示例如下:
[nosetests]
with-doctest=1
doctest-extension=.txt
py.test
py.test 非常类似于 nose。事实上,后者是受到 py.test 的启发,所以我们将主
要集中在些工具彼此不同的细节上。该工具诞生为一个更大的名为 py 包的一部分,但现
在这些是单独开发的。
像本书中提到的每个第三方包一样,py.test 在 PyPI 上也是可用的,所以可以使用
pip 安装 pytest,代码如下:
$ pip install pytest
从那里,一个新的 py.test 命令可以在命令提示符中使用,可以完全像 nosetests
那样使用它。该工具使用类似的模式匹配和测试发现算法捕获要运行的测试。这种模式比
nose 使用的模式更严格并且只会捕获:
● 以 test 开头的文件中的以 test 开头的类;
● 以 test 开头的文件中的以 test 开始的函数。
py.test 的优点如下。
● 能够轻松禁用某些测试类。
● 一个灵活和新颖的处理测试固件的机制。
● 在多台计算机之间分发测试的能力。
编写测试固件
py.test 支持两种机制来处理固件。第一个,在 xUnit 框架之后建模,类似于
nose。当然语义有所不同。py.test 将在每个测试模块中查找 3 个级别的固件,如
下例所示:
def setup_module(module):
“”" 设置任何给定模块的执行的特定的状态。
“”"
def teardown_module(module):
“”" 卸载之前使用 setup_module 方法设置的任何状态。
“”"
def setup_class(cls):
“”" 设置任何给定类(通常包含测试)的执行的特定的状态。
“”"
def teardown_class(cls):
“”" 卸载之前使用 setup_class 方法设置的任何状态。
“”"
def setup_method(self, method):
“”" 设置任何绑定到类中给定方法的执行的状态。
对类的每个测试方法调用 setup_method。
“”"
def teardown_method(self, method):
“”" 卸载之前使用 setup_method 方法设置的任何状态
“”"
每个函数将获取当前模块,类或方法作为参数。因此,测试固件将能够在上下文中工
作,而不必像 nose 那样寻找它。
使用 py.test 编写测试固件的另一种机制是建立在依赖注入的概念上的,允许以更
加模块化和可扩展的方式维护测试状态。非 xUnit 风格的固件(setup / teardown 过程)
总是具有唯一的名称,并且需要通过在类中的测试函数,方法和模块中的声明它们的使用
来显式激活。
最简单的固件实现形式是采用 pytest.fixture()装饰器声明的命名函数。要标记
测试中使用的固件,它需要声明为一个函数或方法参数。为了使它更清楚,把前面的例子
使用 py.test 固件重写 is_prime 函数的测试模块,如下所示:
import pytest
from primes import is_prime
@pytest.fixture()
def prime_numbers():
return [3, 5, 7]
@pytest.fixture()
def non_prime_numbers():
return [8, 0, 1]
@pytest.fixture()
def negative_numbers():
return [-1, -3, -6]
def test_is_prime_true(prime_numbers):
for number in prime_numbers:
assert is_prime(number)
def test_is_prime_false(non_prime_numbers, negative_numbers):
for number in non_prime_numbers:
assert not is_prime(number)
for number in non_prime_numbers:
assert not is_prime(number)
禁用测试函数和类
py.test 提供了一个简单的机制,可以在某些条件下禁用一些测试。这被称为跳过
(skipping),并且 pytest 包提供了.skipif 装饰器支持这种机制。如果在某些条件下需
要跳过单个测试函数或整个测试类装饰器,则需要使用此装饰器并且提供的一些用于验证
是否满足预期条件的值来定义。下面是一个官方文档的实例,在 Windows 上跳过运行整个
测试用例类,如下所示:
import pytest
@pytest.mark.skipif(
sys.platform == ‘win32’,
reason=“does not run on windows”
)
class TestPosixCalls:
def test_function(self):
“”“在 win32 平台上不会启动运行”“”
当然,你可以预定义跳过条件,以便在测试模块中共享它们,如下所示:
import pytest
skipwindows = pytest.mark.skipif(
sys.platform == ‘win32’,
reason=“does not run on windows”
)
@skip_windows
class TestPosixCalls:
def test_function(self):
“”“在 win32 平台上不会启动运行”“”
如果以这种方式标记测试,它将不会被执行。但是,在某些情况下,你想要运行这样的测试并想要执行它,但是你知道,在已知条件下,测试预期是失败的。为此,提供了一
个不同的装饰器。它是@mark.xfail,并确保测试始终运行,但如果发生预定义的条件,
它将在某个点失败,如下所示:
import pytest
@pytest.mark.xfail(
sys.platform == ‘win32’,
reason=“does not run on windows”
)
class TestPosixCalls:
def test_function(self):
“”“在 windows 下,一定运行失败”“”
使用 xfail 比 skipif 更严格。测试总是执行,如果它不是预期的失败,那么整个
py.test 运行将导致失败。
自动化的分布式测试
py.test 的一个有趣的特性是它能够跨多台计算机分发测试。只要计算机可以通过
SSH 访问,py.test 就能够驱动每台计算机,发送测试并执行。
但是,此功能依赖于网络;如果连接断开,服务器将无法继续工作,因为它完全由主
服务器驱动的。
当项目有长时间的测试活动时,Buildbot 或其他持续集成工具则是更好的选择。但是,
当你在需要消耗很多资源来运行测试的应用程序上工作时,py.test 分布式模型可以用于
随机分布测试。
py.test 小结
py.test 非常类似于 nose,它也不需要样板代码来聚合它的测试。它也有一个很好
的插件系统,在 PyPI 上有大量可用的扩展。
最后,py.test 重点在于使测试运行得更快,并且与该领域中的其他工具相比,它真
的更出色。另一个值得注意的特性是新颖的固件方法,真正有助于管理可重用的固件库。
有些人可能认为它有太多的魔法,但它真的简化了测试套件的开发。py.test 的这一个优
势使其成为我的首选工具,所以我真的非常推荐使用它。