有时,更复杂的测试需要在运行要测试的代码之前进行一些设置。可以在测试功能本身中执行此操作,但是最终您将不得不执行大型测试功能,以至于很难分辨设置在何处停止并开始测试。您还可以在各种测试功能之间获得大量重复的设置代码。
我们的代码文件:
# projectroot /模块/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()
夹具是测试设置功能的更强大,更灵活的版本。他们可以做的事情比我们在这里要利用的要多得多,但是我们一次只能迈出一步。
首先,我们将其更改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 /模块/stuff.py class Stuff(object): def prep(self): self.foo= 1 self.bar= 2 def finish(self): self.foo= 0 self.bar= 0
我们可以在每个测试功能的底部添加一些代码来调用清除操作,但是固定装置提供了一种更好的方法来执行此操作。如果您向灯具添加一个功能并将其注册为终结器,则在使用灯具进行测试之后,将调用终结器函数中的代码。如果fixture的范围大于单个功能(例如模块或会话),则完成器将在范围内的所有测试完成后执行,因此在模块运行完毕后或在整个测试运行会话结束时将执行终结器。
@pytest.fixture def prepped_stuff(request): # 我们需要传递使用终结器的请求 my_stuff = stuff.Stuff() my_stuff.prep() def fin(): # 终结函数 # 在这里进行所有清理 my_stuff.finish() request.addfinalizer(fin) # 将fin()注册为终结器 # 如果您确实想在这里进行更多设置 return my_stuff
乍一看,在函数内部使用finalizer函数可能有点难以理解,尤其是当您拥有更复杂的灯具时。您可以改为使用yield固定装置,以更易于理解的执行流程来执行相同的操作。唯一真正的区别是,在设置完成的固定装置部分return使用而不是使用a yield,并且控件应转到测试功能,然后在之后添加所有清除代码yield。我们还将其装饰为,yield_fixture以便py.test知道如何处理它。
@pytest.yield_fixture def prepped_stuff(): # 现在不需要请求! # 做设置 my_stuff = stuff.Stuff() my_stuff.prep() # 设置完成,将控制权传递给测试功能 yield my_stuff # 做清理 my_stuff.finish()
到此为止,测试夹具简介!
有关更多信息,请参见官方py.test夹具文档和官方产量夹具文档。