在pytest中加入fixture的目的是提供一个固定的基准,使测试能够可靠、重复地执行,pytest的fixture比传统xUnit风格的setup/teardown函数相比,有了巨大的改进:
- fixture具有明确的名称,并通过在测试函数、模块、类或整个项目中声明它们的使用来激活。
- fixture是以模块化的方式实现的,因为每个fixture名称都会触发fixture函数,其本身也可以使用其他fixture。
- fixture管理从简单的单元扩展到复杂的函数测试,允许根据配置和组件选项参数化fixture和测试,或者在函数、类、模块或整个测试会话范围内重复使用fixture。
另外,pytest会继续支持经典的xUnit风格的设置,我们可以混合使用这两种风格,按照自己的喜好,从经典风格逐渐过渡到新风格。我们也可以从现有的unittest.TestCase风格或基于nose的项目开始。
将fixture作为函数参数
测试函数可以通过将它们命名为输入参数来接收fixture对象,对于每一个参数名称而言,具有该名称的fixture函数提供fixture对象,fixture的函数是通过用@pytest.fixture
标记来注册的。现在让我们来写一个简单的测试模块,包含一个fixture和一个使用它的测试函数,新建一个test_smtpsimple.py文件,输入以下代码:
import pytest
@pytest.fixture
def smtp():
import smtplib
return smtplib.SMTP("smtp.qq.com", 587, timeout=5)
def test_ehlo(smtp):
response, msg = smtp.ehlo()
assert response == 250
assert 0
在上面代码中,测试test_ehlo需要smtp
fixture的值,pytest会发现并调用@pytest.fixture
标记的smtp
fixture功能,运行测试如下所示:
在失败回溯中,我们可以看到测试函数是用smtp
参数调用的,smtplib.SMTP()
实例是由fixture函数创建的,下面是Pytest用这种方法调用测试函数的过程:
- 由于
test_
前缀,pytest找到了测试函数test_ehlo
,测试函数需要一个名为smtp
的函数参数,通过寻找一个名为smtp
的fixture标记功能来发现匹配的fixture函数。 - 调用
smtp()
来创建一个实例。 -
test_ehlo(<SMTP instance>)
被调用,并在测试函数的最后一行失败。
需要注意的是,如果拼错一个函数参数或想使用一个不可用的函数参数,将会看到一个包含可用函数参数列表的错误。我们可以使用以下命令看到可用的fixture:
pytest --fixtures test_simplefactory.py
使用fixture的依赖注入
fixture允许测试函数轻松接收和处理特定的,预先初始化的应用程序对象,而不必在意导入、设置、清理的细节。这是fixture依赖注入的主要做法,fixture函数是注入器的角色,测试函数是fixture对象的消费者。
如果在执行测试期间,我们意识到要使用来自多个测试文件的fixture函数,您可以将其移至conftest.py
文件。这样一来,我们就不需要导入想在测试中使用的fixture,此时它会自动被pytest发现。pytest函数的发现始于测试类,然后是测试模块,再然后是conftest.py
文件,最后是内置和第三方插件。
如果我们想从测试中获得可用的测试数据,有一个好方法是把这些数据加载到我们的测试中使用,这是pytest的自动缓存机制。或者,另一个好方法是在测试文件夹中添加数据文件。还有一些社区插件可以帮助我们管理这方面的测试,例如pytest-datadir和pytest-datafiles。
在指定范围内共享fixture实例
需要网络访问的fixture取决于连接性,而且创建起来往往花费大量时间,扩展前面的例子,我们可以在@pytest.fixture
调用中添加一个scope='module'
参数,使每个测试模块只能调用一次修饰的smtp
fixture函数,默认是每个测试函数调用一次。测试模块中的多个测试函数因此将分别接收相同的smtp
fixture实例,从而节省时间。
在下面的示例中,我们将fixture函数放入一个单独的conftest.py
文件中,以便目录中多个测试模块的测试都可以访问fixture函数,新建一个conftest.py文件,输入以下代码:
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp():
return smtplib.SMTP("smtp.qq.com", 587, timeout=5)
fixture的名字还是smtp
,我们可以通过列出名称smtp
作为输入参数在任何测试或fixture函数来访问其结果,前提是位于或低于conftest.py
所在的目录,新建一个test_module.py文件,输入以下代码:
def test_ehlo(smtp):
response, msg = smtp.ehlo()
assert response == 250
assert b"smtp.qq.com" in msg
assert 0
def test_noop(smtp):
response, msg = smtp.noop()
assert response == 250
assert 0
我们故意插入失败的assert 0
语句来检查正在发生的事情,现在可以运行测试:
我们可以看到两个assert 0
失败,更重要的是,我们也可以看到同一模块范围内的smtp
对象被传递到两个测试函数中,因为pytest显示了回溯中的传入参数值。因此,使用smtp
的两个测试函数的运行速度与单个测试函数一样快,因为它们重用了相同的实例。
如果我们希望拥有一个会话范围的smtp
实例,则可以简单地声明它,这样的话,类范围将在每个测试类中调用一次fixture:
@pytest.fixture(scope="session")
def smtp(...):
fixture的完成与拆卸代码
当fixture超出范围时,pytest支持执行fixture特定的最终代码,通过使用yield
语句而不是return
,yield
语句之后的所有代码都用作拆卸代码,修改conftest.py
的代码:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp():
smtp = smtplib.SMTP("smtp.qq.com", 587, timeout=5)
yield smtp
print("拆卸smtp")
smtp.close()
print
和smtp.close()
语句将在模块的最后一次测试完成后执行,不管测试的异常状态如何,让我们来执行它:
我们看到,在两个测试完成执行后,smtp
实例已经完成,需要注意的是,如果我们使用scope='function'
来修饰fixture函数,那么fixture设置和清理将在每个单独的测试中发生。在任何一种情况下,测试模块本身都不需要改变或了解fixture设置的这些细节。
同样,我们也可以通过with
语句无缝地使用yield
语法,这样测试完成后,smtp
连接将被关闭,因为当with
语句结束时,smtp
对象会自动关闭:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp():
with smtplib.SMTP("smtp.qq.com", 587, timeout=5) as smtp:
yield smtp
需要注意一下,如果在设置代码,即yield
关键字之前,期间发生异常,则不会调用拆卸代码,即即yield
关键字之后的代码。执行拆卸代码的另一种选择是利用请求上下文对象的addfinalizer
方法来注册完成函数。例如,下面的smtp
fixture更改为使用addfinalizer
进行清理:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp(request):
smtp = smtplib.SMTP("smtp.qq.com", 587, timeout=5)
def fin():
print ("拆卸smtp")
smtp.close()
request.addfinalizer(fin)
return smtp
yield
和addfinalizer
方法在测试结束后通过调用它们的代码来工作,但addfinalizer
与yield
相比有两个关键的区别。第一点是,可以注册多个完成函数。第二点是,无论fixture设置代码是否引发异常,完成函数将始终被调用,即使其中一个未能创建与获取,也可以正确关闭由fixture创建的所有资源:
@pytest.fixture
def equipments(request):
r = []
for port in ('C1', 'C3', 'C28'):
equip = connect(port)
request.addfinalizer(equip.disconnect)
r.append(equip)
return r
在上面的代码中,如果“C28”发生异常,“C1”和“C3”仍然会被正确关闭。当然,如果在完成函数注册之前发生异常,那么它将不会被执行。
fixture反向获取请求的测试环境
fixture函数可以通过接受request
对象来反向获取请求中的测试函数、类或模块上下文,进一步扩展之前的smtp
fixture示例,让我们从fixture的测试模块读取可选的服务器URL:
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp(request):
server = getattr(request.module, "smtpserver", "smtp.qq.com")
smtp = smtplib.SMTP(server, 587, timeout=5)
yield smtp
print ("完成 %s (%s)" % (smtp, server))
smtp.close()
我们使用request.module
属性来从测试模块中选择性地获取smtpserver
属性,如果我们再次执行,没有什么改变:
再让我们快速创建另一个测试模块,在其模块名称空间中实际设置服务器URL,新建一个test_anothersmtp.py文件,输入以下代码:
smtpserver = "mail.python.org"
def test_showhelo(smtp):
assert 0, smtp.helo()
然后我们运行它:
大家可以看到,smtp
fixture函数从模块名称空间中选取我们的邮件服务器名称。
参数化fixture
fixture函数可以参数化,在这种情况下,它们将被多次调用,每次执行一组相关测试,即依赖于这个fixture的测试,测试函数通常不需要知道它们的重新运行。fixture参数化可以用于一些有多种方式配置的功能测试。
扩展前面的例子,我们可以标记fixture来创建两个smtp
fixture实例,这将导致使用fixture的所有测试运行两次,fixture函数通过特殊的request
对象访问每个参数:
import pytest
import smtplib
@pytest.fixture(scope="module",
params=["smtp.qq.com", "mail.python.org"])
def smtp(request):
smtp = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp
print ("完成 %s" % smtp)
smtp.close()
主要的变化是使用@pytest.fixture
声明params
,这是fixture函数将执行的每个值的列表,并且可以通过request.param
访问一个值,没有测试函数代码需要改变,所以让我们执行一次:
我们可以看到,我们的两个测试函数每个都运行了两次,而且是针对不同的smtp
实例。pytest将建立一个字符串,它是参数化fixture中每个fixture值的测试ID,在上面的例子中,test_ehlo[smtp.qq.com]
和test_ehlo[mail.python.org]
,这些ID可以与-k
一起使用来选择要运行的特定实例,还可以在发生故障时识别特定实例。使用--collect-only
运行pytest会显示生成的ID。
数字、字符串、布尔值和None将在测试ID中使用其通常的字符串表示形式,对于其他对象,pytest会根据参数名称创建一个字符串,可以通过使用ids
关键字参数来自定义用于测试ID的字符串。新建一个test_ids.py文件,输入以下代码:
import pytest
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
return request.param
def test_a(a):
pass
def idfn(fixture_value):
if fixture_value == 0:
return "eggs"
else:
return None
@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
return request.param
def test_b(b):
pass
上面显示了ids
可以是一个要使用的字符串列表,还可以是一个将用fixture值调用的函数,然后返回一个字符串来使用。在后一种情况下,如果函数返回None
,那么将使用pytest的自动生成的ID。运行上述测试会导致使用以下测试ID: