测试夹具简介

在运行要测试的代码之前,有时需要设置更复杂的测试。可以在测试功能本身中执行此操作,但最终你需要执行大量测试功能,以至于很难确定设置停止和测试开始的位置。你还可以在各种测试功能之间获得大量重复的设置代码。

我们的代码文件:

# 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 夹具文档