Section 193.1:设置 py.test
py.test 是 Python 中可用的多个第三方测试库之一。它可以通过 pip 安装:
pip install pytest
要测试的代码
假设我们在 projectroot/module/code.py 中测试一个加法函数:
# projectroot/module/code.py
def add(a, b):
return a + b
测试代码
我们在 projectroot/tests/test_code.py 中创建一个测试文件。文件名必须以 test_ 开头,才能被识别为测试文件。
# projectroot/tests/test_code.py
from module import code
def test_add():
assert code.add(1, 2) == 3
运行测试
在 projectroot 中,我们只需运行 py.test:
# ensure we have the modules
$ touch tests/__init__.py
$ touch module/__init__.py
$ py.test
================================================== test session starts
===================================================
platform darwin -- Python 2.7.10, pytest-2.9.2, py-1.4.31, pluggy-0.3.1
rootdir: /projectroot, inifile:
collected 1 items
tests/test_code.py .
================================================ 1 passed in 0.01 seconds
================================================
Section 193.2: 测试用例的介绍
更复杂的测试有时需要在运行要测试的代码之前设置一些东西。可以在测试函数本身中完成此操作,但随后您会得到庞大的测试函数,这些函数执行了太多操作,以至于很难区分设置在哪里结束,测试在哪里开始。您还可以在不同的测试函数之间获得大量重复的设置代码。
我们的代码文件:
# projectroot/module/stuff.py
class Stuff(object):
def prep(self):
self.foo = 1
self.bar = 2
我们的测试文件:
# projectroot/tests/test_stuff.py
import pytest
from module import stuff
def test_foo_updates():
my_stuff = stuff.Stuff()
my_stuff.prep()
assert 1 == my_stuff.foo
my_stuff.foo = 30000
assert my_stuff.foo == 30000
def test_bar_updates():
my_stuff = stuff.Stuff()
my_stuff.prep()
assert 2 == my_stuff.bar
my_stuff.bar = 42
assert 42 == my_stuff.bar
这些是比较简单的示例,但如果我们的 Stuff 对象需要更多的设置,它将变得难以管理。我们看到在测试用例之间有一些重复的代码,因此让我们首先将这些代码重构到一个单独的函数中。
# projectroot/tests/test_stuff.py
import pytest
from module import stuff
def get_prepped_stuff():
my_stuff = stuff.Stuff()
my_stuff.prep()
return my_stuff
def test_foo_updates():
my_stuff = get_prepped_stuff()
assert 1 == my_stuff.foo
my_stuff.foo = 30000
assert my_stuff.foo == 30000
def test_bar_updates():
my_stuff = get_prepped_stuff()
assert 2 == my_stuff.bar
my_stuff.bar = 42
assert 42 == my_stuff.bar
这看起来更好了,但 my_stuff = get_prepped_stuff() 调用仍然使我们的测试函数显得杂乱。
py.test 测试用例来帮忙!
测试用例是测试设置函数的更强大、更灵活的版本。它们可以做比我们在这里利用的更多的事情,但我们将一步一步来。
首先,我们将 get_prepped_stuff 改为一个名为 prepped_stuff 的测试用例。您希望用名词而不是动词来命名测试用例,因为稍后测试用例将在测试函数本身中被使用。@pytest.fixture 表示这个特定函数应该被当作一个测试用例而不是一个普通函数来处理。
@pytest.fixture
def prepped_stuff():
my_stuff = stuff.Stuff()
my_stuff.prep()
return my_stuff
现在,我们应该更新测试函数,以便它们使用测试用例。这是通过在它们的定义中添加一个与测试用例名称完全匹配的参数来完成的。当 py.test 执行时,它将在运行测试之前运行测试用例,然后通过该参数将测试用例的返回值传递给测试函数。(请注意,测试用例不需要返回一个值;它们可以执行其他设置操作,比如调用外部资源、在文件系统中安排事物、在数据库中放置值,无论测试需要什么设置。)
def test_foo_updates(prepped_stuff):
my_stuff = prepped_stuff
assert 1 == my_stuff.foo
my_stuff.foo = 30000
assert my_stuff.foo == 30000
def test_bar_updates(prepped_stuff):
my_stuff = prepped_stuff
assert 2 == my_stuff.bar
my_stuff.bar = 42
assert 42 == my_stuff.bar
现在您可以看到为什么我们用名词来命名它了。但 my_stuff = prepped_stuff 这一行基本上是多余的,所以让我们直接使用 prepped_stuff。
def test_foo_updates(prepped_stuff):
assert 1 == prepped_stuff.foo
prepped_stuff.foo = 30000
assert prepped_stuff.foo == 30000
def test_bar_updates(prepped_stuff):
assert 2 == prepped_stuff.bar
prepped_stuff.bar = 42
assert 42 == prepped_stuff.bar
现在我们正在使用测试用例!我们可以通过改变测试用例的作用域(使其在每次测试模块或测试套件执行会话中只运行一次,而不是每次测试函数运行一次),构建使用其他测试用例的测试用例,对测试用例进行参数化(这样测试用例以及使用该测试用例的所有测试将针对每个参数运行多次),从调用它们的模块中读取值的测试用例……如前所述,测试用例比普通设置函数具有更多的功能和灵活性。
测试完成后进行清理。
假设我们的代码已经增长,我们的 Stuff 对象现在需要特殊的清理。
# projectroot/module/stuff.py
class Stuff(object):
def prep(self):
self.foo = 1
self.bar = 2
def finish(self):
self.foo = 0
self.bar = 0
我们可以在每个测试函数的底部添加一些代码来调用清理,但测试用例提供了一种更好的方法。如果您在测试用例中添加一个函数并将其注册为终结器,那么在使用该测试用例的测试完成之后,终结器函数中的代码将被调用。如果测试用例的作用域大于单个函数(比如模块或会话),终结器将在作用域内的所有测试完成之后执行,因此在模块运行完成或整个测试运行会话结束时执行。
@pytest.fixture
def prepped_stuff(request): # 我们需要传递 request 来使用终结器
my_stuff = stuff.Stuff()
my_stuff.prep()
def fin(): # 终结器函数
# 在这里进行所有清理工作
my_stuff.finish()
request.addfinalizer(fin) # 将 fin() 注册为终结器
# 如果您真的想在这里进行更多设置,可以继续进行
return my_stuff
在函数内部使用终结器函数可能一开始有点难以理解,尤其是当您有更复杂的测试用例时。您可以改用 yield 测试用例,以更易于人类阅读的执行流程来完成相同的操作。唯一的真正区别是,我们不在这里使用 return,而是使用 yield 在完成设置并应将控制权交给测试函数的部分,然后在 yield 之后添加所有清理代码。我们还将它装饰为 yield_fixture,以便 py.test 知道如何处理它。
@pytest.yield_fixture
def prepped_stuff(): # 现在不需要 request 了!
# 进行设置
my_stuff = stuff.Stuff()
my_stuff.prep()
# 设置完成,将控制权交给测试函数
yield my_stuff
# 进行清理
my_stuff.finish()
这就结束了测试用例的介绍!
有关更多信息,请参阅官方的 py.test 测试用例文档和官方的 yield 测试用例文档。
Section 193.3: 失败的测试
失败的测试将提供有关出错原因的有用输出:
# projectroot/tests/test_code.py
from module import code
def test_add__failing():
assert code.add(10, 11) == 33
结果:
$ py.test
================================================== test session starts
===================================================
platform darwin -- Python 2.7.10, pytest-2.9.2, py-1.4.31, pluggy-0.3.1
rootdir: /projectroot, inifile:
collected 1 items
tests/test_code.py F
======================================================== FAILURES
========================================================
___________________________________________________ test_add__failing
____________________________________________________
def test_add__failing():
> assert code.add(10, 11) == 33
E assert 21 == 33
E + where 21 = <function add at 0x105d4d6e0>(10, 11)
E + where <function add at 0x105d4d6e0> = code.add
tests/test_code.py:5: AssertionError
================================================ 1 failed in 0.01 seconds
================================================