测试夹具简介
在运行要测试的代码之前,有时需要设置更复杂的测试。可以在测试功能本身中执行此操作,但最终你需要执行大量测试功能,以至于很难确定设置停止和测试开始的位置。你还可以在各种测试功能之间获得大量重复的设置代码。
我们的代码文件:
# 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 执行时,它将在运行测试之前运行 fixture,然后通过该参数将 fixture 的返回值传递给测试函数。 (请注意,灯具不需要返回值;它们可以改为执行其他设置操作,例如调用外部资源,在文件系统上安排内容,将值放入数据库,无论设置需要什么测试)
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
我们可以添加一些代码来调用每个测试函数底部的清理,但是 fixtures 提供了一种更好的方法来实现这一点。如果向夹具添加一个函数并将其注册为终结器,则在使用夹具完成测试后,将调用终结器函数中的代码。如果 fixture 的范围大于单个函数(如模块或会话),则在范围内的所有测试完成后执行终结器,因此在模块完成运行后或在整个测试运行会话结束时。
@pytest.fixture
def prepped_stuff(request): # we need to pass in the request to use finalizers
my_stuff = stuff.Stuff()
my_stuff.prep()
def fin(): # finalizer function
# do all the cleanup here
my_stuff.finish()
request.addfinalizer(fin) # register fin() as a finalizer
# you can do more setup here if you really want to
return my_stuff
使用函数内部的终结器功能乍一看有点难以理解,特别是当你有更复杂的灯具时。你可以使用 yield fixture 来使用更易读的执行流来执行相同的操作。唯一真正的区别是,我们不是使用 return
,而是在灯具的一部分使用 yield
进行设置,控制应该进入测试功能,然后在 yield
之后添加所有清理代码。我们还将它装饰成 yield_fixture
,以便 py.test 知道如何处理它。
@pytest.yield_fixture
def prepped_stuff(): # it doesn't need request now!
# do setup
my_stuff = stuff.Stuff()
my_stuff.prep()
# setup is done, pass control to the test functions
yield my_stuff
# do cleanup
my_stuff.finish()
这就是测试夹具简介的结论!
有关更多信息,请参阅官方 py.test fixture 文档和官方 yield 夹具文档