Pytest 固件:显式、模块化、可扩展
purpose of test fixtures是提供一个固定的基线,在此基础上测试可以可靠地重复执行。Pytest固件比传统的XUnit 的setup/teardown功能提供了显著的改进:
此外,pytest继续支持 经典的Xunit-style setup. 你可以混合这两种样式,根据喜好,逐步从经典样式转移到新样式。你也可以从现有的 unittest.TestCase style 或 nose based 项目开始。
fixtures作为函数参数
测试函数可以通过将fixture对象命名为输入参数来接收它们。对于每个参数名,具有该名称的fixture函数提供fixture对象。通过用@pytest.fixture
标记fixture函数来注册fixture函数 . 让我们来看一个简单的独立测试模块,它包含一个fixture和一个使用fixture的测试函数:
# content of ./test_smtpsimple.py
import pytest
@pytest.fixture
def smtp_connection():
import smtplib
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert 0 # for demo purposes
这里, test_ehlo
需要 smtp_connection
固件值。Pytest将发现并调用 @pytest.fixture
标记 smtp_connection
固件函数。运行测试的方式如下:
$ pytest test_smtpsimple.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item
test_smtpsimple.py F [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_smtpsimple.py:11: AssertionError
========================= 1 failed in 0.12 seconds =========================
在失败的回溯中,我们看到测试函数是用 smtp_connection
参数、 fixture函数创建的实例smtplib.SMTP()
来调用的。测试功能失败是故意使用了 assert 0
. pytest
以这种方式来调用测试函数:
test_ehlo
因为 test_
前缀。这个测试函数需要一个名为 smtp_connection
. 通过查找名为 smtp_connection
标记的固件函数,找到一个匹配的固件函数.smtp_connection()
通过创建实例来调用。test_ehlo()
在测试函数的最后一行调用并失败。请注意,如果你拼错了一个函数参数,或者希望使用一个不可用的参数,你将看到一个错误,其中包含一个可用函数参数列表。
注解
你可以随时发布:
pytest --fixtures test_simplefactory.py
查看可用的固件(带引线的固件 _
仅当添加 -v
选择权。
固件:依赖注入的主要示例
fixture允许测试函数轻松地接收和处理特定的预初始化应用程序对象,而不必关心import/setup/teardown细节。这是一个dependency injection的主要例子,其中fixture函数扮演的是injector(注入), 测试函数是固件对象的consumers(消费者) 。
共享固件功能
如果在实现测试的过程中,你意识到要使用来自多个测试文件的fixture函数,可以将其移动到 conftest.py
文件。你不需要导入要在测试中使用的固件,Pytest会发现并自动获取它。fixture函数的发现从测试类开始,然后是测试模块,然后 conftest.py
文件,最后是内置插件和第三方插件。
你也可以使用 conftest.py
文件去实现 local per-directory plugins.
共享测试数据
如果你想让来自文件的测试数据对你的测试可用,一个很好的方法是将这些数据加载到一个固件中,供测试使用。这利用了pytest的自动缓存机制。
另一个好方法是将数据文件添加到 tests
文件夹中. 这里也有可用的插件社区可以用来帮助管理这方面的testing e.g. pytest-datadir 和 pytest-datafiles.
范围:在类、模块或会话中跨测试共享一个fixture实例
依赖于连接性的需要访问网络的fixtures,通常创建成本很高。扩展前面的示例,我们可以给 @pytest.fixture
调用添加一个 scope="module"
参数,引起被装饰的 smtp_connection
固件函数在每个测试模块中只调用一次(默认情况下,每个测试调用一次 function)因此,一个测试模块中的多个测试功能将接收相同的 smtp_connection
固件实例(不必每次都实例化,也就是不用每次都去创建连接访问网络),节省时间提高效率。 对于scope
可能值是: function
, class
, module
, package
或 session
.
下一个示例将fixture函数放入单独的 conftest.py
文件,以便目录中多个测试模块的测试可以访问fixture函数:
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
固件的名称同样是 smtp_connection
,你可以通过列出名字smtp_connection
作为一个在任何测试或fixture函数的输入参数(在 conftest.py
所在的目录中,或者所在的目录下)来访问它的结果 :
# content of test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
assert 0 # for demo purposes
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
我们故意插入失败 assert 0
语句以检查正在进行的操作,现在可以运行测试:
$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 2 items
test_module.py FF [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
E assert 0
test_module.py:6: AssertionError
________________________________ test_noop _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:11: AssertionError
========================= 2 failed in 0.12 seconds =========================
你看这两个 assert 0
失败,更重要的是,你也可以看到相同的(module-scoped模块范围) smtp_connection
对象被传递到两个测试函数中,因为pytest在回溯中显示了传入的参数值。因此,两个测试函数使用 smtp_connection
像单个实例一样快速运行,因为它们重用同一个实例。
如果你决定希望有一个会话范围装饰的 smtp_connection
实例, 例如,你可以简单地声明它:
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests needing it
...
最后, class
作用域将在每个测试class中调用fixture一次 .
注解
pytest一次只缓存一个fixture实例。这意味着当使用参数化固件时,pytest可以在给定的范围内多次调用固件。
package
scope(experimental)
3.7 新版功能.
在Pytest 3.7中, package
范围已引入。当最后一次package测试结束时,Package-scoped fixtures 完成。
警告
考虑到该功能时 实验性的 ,如果在将来的版本中发现隐藏的角落情况或此功能的严重问题,则可能将其删除。
请谨慎使用此新功能,并确保报告你发现的任何问题。
首先实例化更大范围的固件
在特性的功能请求中,更高范围的固件(例如 session
)先实例化,然后再实例化范围较低的固件(例如 function
或 class
)。相同范围内固件的相对顺序遵循测试函数中声明的顺序,并尊重固件之间的依赖关系。
考虑下面的代码:
@pytest.fixture(scope="session")
def s1():
pass
@pytest.fixture(scope="module")
def m1():
pass
@pytest.fixture
def f1(tmpdir):
pass
@pytest.fixture
def f2():
pass
def test_foo(f1, m1, f2, s1):
...
由 test_foo
所请求的固件将按以下顺序实例化:
s1
:是范围最高的固件 (session
)m1
:是第二高范围固件 (module
)tmpdir
是一个 function
范围固件, f1
需要 :因为它是f1
的一个依赖项,此时它需要实例化 .f1
是第一个在 test_foo
参数列表内的 function
-范围固件。f2
是最后一个在 test_foo
参数列表内的 function
-范围固件。固件定型/执行拆卸代码
当fixture超出范围时,pytest支持fixture特定的定稿代码的执行。通过使用 yield
语句而不是 return
, yield语句之后的所有代码当做是teardown代码:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
yield smtp_connection # provide the fixture value
print("teardown smtp")
smtp_connection.close()
这个 print
和 smtp.close()
语句将在模块中的最后一个测试完成执行后执行,而不管测试的异常状态如何。
让我们执行它:
$ pytest -s -q --tb=no
FFteardown smtp
2 failed in 0.12 seconds
我们看到了 smtp_connection
实例在两个测试完成执行后完成。请注意,如果我们用 scope='function'
来声明固件函数, 然后在每个测试周围进行fixture setup和clearup。无论哪种情况,测试模块本身都不需要更改或了解这些固件setup的细节。
请注意,我们还可以无缝地使用 yield
语法与 with
声明:
# content of test_yield2.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
yield smtp_connection # provide the fixture value
这个 smtp_connection
连接将在测试完成执行后关闭,因为 smtp_connection
对象会自动关闭当 with
语句结束时。
请注意,如果一个异常在 setup
代码(在 yield
关键字前)中发生,那么 teardown 代码(在 yield
后)不会被调用。
执行 teardown
代码的替代选项是利用 addfinalizer
方法(用request-context对象注册终结函数)。
这里是 smtp_connection
固件变化地使用addfinalizer
清理:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection(request):
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def fin():
print("teardown smtp_connection")
smtp_connection.close()
request.addfinalizer(fin)
return smtp_connection # provide the fixture value
相似地, yield
和 addfinalizer
方法都是通过在测试结束后调用它们的代码来工作,但是 addfinalizer
有两个关键差异点区别于 yield
:
可以注册多个终结器函数。
无论fixture的 setup 代码是否引发异常,finalizers终结器都会被调用。这对于正确关闭由固件创建的所有资源非常方便,即使其中一个资源未能创建/获取:
@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"
仍将正确关闭。当然,如果在注册finalize函数之前发生异常,则不会执行它。
fixtures可以反省请求的测试上下文
固件函数可以接受 request
对象内省“requesting”测试函数、类或模块上下文。进一步扩展前一个 smtp_connection
fixture示例,让我们从使用fixture的测试模块中读取一个可选的服务器URL:
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
yield smtp_connection
print("finalizing %s (%s)" % (smtp_connection, server))
smtp_connection.close()
我们使用 request.module
属性来选择获取一个来自测试模块的smtpserver
属性。如果我们再执行一次,没有什么改变:
$ pytest -s -q --tb=no
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
2 failed in 0.12 seconds
让我们快速创建另一个测试模块,该模块在其模块命名空间中实际设置服务器URL:
# content of test_anothersmtp.py
smtpserver = "mail.python.org" # will be read by smtp fixture
def test_showhelo(smtp_connection):
assert 0, smtp_connection.helo()
运行它:
$ pytest -qq --tb=short test_anothersmtp.py
F [100%]
================================= FAILURES =================================
______________________________ test_showhelo _______________________________
test_anothersmtp.py:5: in test_showhelo
assert 0, smtp_connection.helo()
E AssertionError: (250, b'mail.python.org')
E assert 0
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef> (mail.python.org)
这个 smtp_connection
fixture函数从模块名称空间中获取邮件服务器名称。
工厂作为固件
“工厂作为固件”模式有助于在单个测试中多次需要固件结果的情况下。固件不直接返回数据,而是返回一个生成数据的函数。然后可以在测试中多次调用此函数。
工厂可以根据需要设置参数:
@pytest.fixture
def make_customer_record():
def _make_customer_record(name):
return {
"name": name,
"orders": []
}
return _make_customer_record
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
如果工厂创建的数据需要管理,则固件可以处理:
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):
record = models.Customer(name=name, orders=[])
created_records.append(record)
return record
yield _make_customer_record
for record in created_records:
record.destroy()
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
参数化固件
fixture函数可以参数化,在这种情况下,它们将被多次调用,每次执行一组相关的测试,即测试依赖于该fixture。测试函数通常不需要知道它们的重新运行。固件参数化有助于为组件编写详尽的功能测试,这些组件本身可以通过多种方式进行配置。
扩展前面的示例,我们可以标记fixture以创建两个 smtp_connection
fixture实例,它将导致使用fixture的所有测试运行两次。fixture函数通过特殊的 request
对象来获取每个参数:
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module",
params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print("finalizing %s" % smtp_connection)
smtp_connection.close()
主要变化是 params
具有 @pytest.fixture
,fixture函数将执行的每个值的列表,可以通过 request.param
访问每一个值 . 无需更改测试函数代码。让我们再运行一次:
$ pytest -q test_module.py
FFFF [100%]
================================= FAILURES =================================
________________________ test_ehlo[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
E assert 0
test_module.py:6: AssertionError
________________________ test_noop[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:11: AssertionError
________________________ test_ehlo[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert b"smtp.gmail.com" in msg
E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING'
test_module.py:5: AssertionError
-------------------------- Captured stdout setup ---------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef>
________________________ test_noop[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:11: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef>
4 failed in 0.12 seconds
我们看到我们的两个测试函数分别运行两次,针对的是不同的 smtp_connection
实例。还要注意的是, 在test_ehlo
中mail.python.org
连接第二次测试失败,因为预期的服务器字符串与实际获取的字符串不同。
pytest将构建一个字符串,该字符串是参数化fixture中每个fixture值的测试ID,例如在在上面的例子中的test_ehlo[smtp.gmail.com]
和 test_ehlo[mail.python.org]
。这些ID可用于 -k
选择要运行的特定案例,当某个案例失败时,它们还将识别该特定案例。使用pytest --collect-only
运行将显示生成的ID。
数字、字符串、布尔值和None将在测试ID中使用它们通常的字符串表示形式。对于其他对象,pytest将根据参数名生成字符串。在一个测试ID中,可以通过使用 ids
关键字参数来为一个确定的fixture值定制字符串:
# content of 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:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 10 items
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>
<Module test_ids.py>
<Function test_a[spam]>
<Function test_a[ham]>
<Function test_b[eggs]>
<Function test_b[1]>
<Module test_module.py>
<Function test_ehlo[smtp.gmail.com]>
<Function test_noop[smtp.gmail.com]>
<Function test_ehlo[mail.python.org]>
<Function test_noop[mail.python.org]>
======================= no tests ran in 0.12 seconds =======================
对参数化固件使用标记
pytest.param()
可在参数化固件的值集里应用于标记,与它们可用于@pytest.mark.parametrize 的方法相同 .
例子::
# content of test_fixture_marks.py
import pytest
@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):
return request.param
def test_data(data_set):
pass
运行此测试将 skip 调用 data_set
有价值 2
:
$ pytest test_fixture_marks.py -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 3 items
test_fixture_marks.py::test_data[0] PASSED [ 33%]
test_fixture_marks.py::test_data[1] PASSED [ 66%]
test_fixture_marks.py::test_data[2] SKIPPED [100%]
=================== 2 passed, 1 skipped in 0.12 seconds ====================
模块化:使用fixture函数中的fixtures
不仅可以在测试函数中使用fixture,fixture函数还可以使用其他fixture本身。这有助于固件fixtures的模块化设计,并允许在许多项目中重用特定框架的固件。作为一个简单的示例,我们可以扩展前面的示例并实例化一个对象 app
,我们把已经定义好的 smtp_connection
资源加入进去了:
# content of test_appsetup.py
import pytest
class App(object):
def __init__(self, smtp_connection):
self.smtp_connection = smtp_connection
@pytest.fixture(scope="module")
def app(smtp_connection):
return App(smtp_connection)
def test_smtp_connection_exists(app):
assert app.smtp_connection
我们在此声明 app
接收先前定义的 smtp_connection
fixture并实例化 App
对象。让我们运行它:
$ pytest -v test_appsetup.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 2 items
test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%]
test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%]
========================= 2 passed in 0.12 seconds =========================
由于参数化 smtp_connection
,测试将运行两次 App
实例和相应的SMTP服务器。 app
固件没有必要应注意 smtp_connection
参数化,因为Pytest将充分分析固件依赖关系图。
请注意 app
固件的 module
范围,并使用 smtp_connection
模块范围固件。如果 smtp_connection
被缓存在 session
范围也是可行的:fixture可以使用“更广”范围的fixture,但不能使用另一种方式:会话session范围的fixture不能以有意义的方式使用模块module范围的fixture。
按fixture实例自动分组测试
在测试运行期间,pytest最小化了活跃fixtures的数量。如果你有一个参数化的fixture,那么使用它的所有测试将首先用一个实例执行,然后在创建下一个fixture实例之前调用终结器。此外,这简化了对创建和使用全局状态的应用程序的测试。
下面的示例使用两个参数化的fixture,其中一个在每个模块的基础上确定范围,所有功能都执行 print
调用以显示setup/teardown流:
# content of test_module.py
import pytest
@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
param = request.param
print(" SETUP modarg %s" % param)
yield param
print(" TEARDOWN modarg %s" % param)
@pytest.fixture(scope="function", params=[1,2])
def otherarg(request):
param = request.param
print(" SETUP otherarg %s" % param)
yield param
print(" TEARDOWN otherarg %s" % param)
def test_0(otherarg):
print(" RUN test0 with otherarg %s" % otherarg)
def test_1(modarg):
print(" RUN test1 with modarg %s" % modarg)
def test_2(otherarg, modarg):
print(" RUN test2 with otherarg %s and modarg %s" % (otherarg, modarg))
让我们在详细模式下运行测试,并查看打印输出:
$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 8 items
test_module.py::test_0[1] SETUP otherarg 1
RUN test0 with otherarg 1
PASSED TEARDOWN otherarg 1
test_module.py::test_0[2] SETUP otherarg 2
RUN test0 with otherarg 2
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod1] SETUP modarg mod1
RUN test1 with modarg mod1
PASSED
test_module.py::test_2[mod1-1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod1
PASSED TEARDOWN otherarg 1
test_module.py::test_2[mod1-2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod1
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod2] TEARDOWN modarg mod1
SETUP modarg mod2
RUN test1 with modarg mod2
PASSED
test_module.py::test_2[mod2-1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod2
PASSED TEARDOWN otherarg 1
test_module.py::test_2[mod2-2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod2
PASSED TEARDOWN otherarg 2
TEARDOWN modarg mod2
========================= 8 passed in 0.12 seconds =========================
可以看到参数化模块作用域 modarg
资源导致测试执行的顺序,从而导致可能的“活动”资源最少。为 mod1
参数化的资源的终结器在 mod2
资源setup前被执行。
特别要注意,test_0是完全独立的,最先完成。然后是使用 mod1
的test_1执行,接着是使用 mod1
的test_2 ,然后是用 mod2
的test_1, 最后是用 mod2
的test_2.
这个 otherarg
参数化资源(具有函数范围)在使用它的每个测试之前setup,然后在使用它的每个测试之后teardown。
使用类、模块或项目中的设备
有时测试函数不需要直接访问fixture对象。例如,测试可能需要使用空目录作为当前工作目录进行操作,否则不关心具体目录。以下是如何使用标准 tempfile 和pytest fixtures 来实现它。我们将创建的fixture分入conftest.py文件:
# content of conftest.py
import pytest
import tempfile
import os
@pytest.fixture()
def cleandir():
newpath = tempfile.mkdtemp()
os.chdir(newpath)
并通过一个 usefixtures
标记,在测试module中声明它的使用:
# content of test_setenv.py
import os
import pytest
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit(object):
def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == []
with open("myfile", "w") as f:
f.write("hello")
def test_cwd_again_starts_empty(self):
assert os.listdir(os.getcwd()) == []
由于 usefixtures
标记,每个测试方法的执行都需要 cleandir
fixture,就像你为每个方法指定了“cleandir”函数参数一样。让我们运行它来验证fixture是否激活,测试是否通过:
$ pytest -q
.. [100%]
2 passed in 0.12 seconds
可以这样指定多个fixtures:
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
...
可以使用标记机制的通用特性,在测试模块级别指定fixture使用:
pytestmark = pytest.mark.usefixtures("cleandir")
注意分配的变量 必须 被称为 pytestmark
,例如 foomark
不会激活fixtures。
也可以将项目中所有测试所需的fixtures放入一个ini文件中:
# content of pytest.ini
[pytest]
usefixtures = cleandir
警告
注意这个标记在 fixture functions里没有作用 . 例如,这个 无法按预期工作 :
@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture():
...
目前,这不会产生任何错误或警告.
自动固定装置(Xunit类固醇装置)
有时,你可能希望在不显式声明函数参数或 usefixtures装饰器的情况下自动调用fixtures。作为一个实际的例子,假设我们有一个数据库设备,它有一个begin/rollback/commit体系结构,并且我们希望通过一个transaction和一个rollback自动包围每个测试方法。下面是这个想法的一个虚拟的独立实现:
# content of test_db_transact.py
import pytest
class DB(object):
def __init__(self):
self.intransaction = []
def begin(self, name):
self.intransaction.append(name)
def rollback(self):
self.intransaction.pop()
@pytest.fixture(scope="module")
def db():
return DB()
class TestClass(object):
@pytest.fixture(autouse=True)
def transact(self, request, db):
db.begin(request.function.__name__)
yield
db.rollback()
def test_method1(self, db):
assert db.intransaction == ["test_method1"]
def test_method2(self, db):
assert db.intransaction == ["test_method2"]
类级别 transact
固件标有 autouse=true 这意味着类中的所有测试方法都将使用这个fixture,而不需要在测试函数签名或类级别的usefixture
装饰器中声明它。
如果我们运行它,我们会得到两个通过的测试:
$ pytest -q
.. [100%]
2 passed in 0.12 seconds
以下是Autouse 固件在其他范围中的工作方式:
scope=
关键字参数:如果一个autouse fixture具有 scope='session'
,无论它在何处被定义,它将只运行一次。而 scope='class'
意味着它将每个类运行一次,等等。注意上面 transact
fixture很可能是你希望在项目中可用的一个fixture,而通常不需要它处于活动状态。实现这一点的规范方法是将transact的定义放入没有使用 autouse
的conftest.py 中:
# content of conftest.py
@pytest.fixture
def transact(request, db):
db.begin()
yield
db.rollback()
然后,例如,通过声明需求,让一个testclass使用它:
@pytest.mark.usefixtures("transact")
class TestClass(object):
def test_method1(self):
...
此TestClass中的所有测试方法都将使用transact fixture,而模块中的其他测试类或函数将不使用它,除非它们还添加了 transact
参考。
覆盖不同级别的设备
在相对较大的测试套中,你很可能需要 override
一个 global
或 root
fixture与 locally
定义一个,用来保持测试代码的可读性和可维护性。
覆盖文件夹(conftest)级别的fixture
假设测试文件结构为:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
def test_username(username):
assert username == 'username'
subfolder/
__init__.py
conftest.py
# content of tests/subfolder/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something.py
# content of tests/subfolder/test_something.py
def test_username(username):
assert username == 'overridden-username'
以上示例,具有相同名称的fixture可以被某些测试文件夹级别的覆盖。请注意 base
或 super
fixture可从 在上面的例子中很容易使用的overriding
fixture中获取。
在测试模块级别上覆盖fixture
假设测试文件结构为:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
def test_username(username):
assert username == 'overridden-username'
test_something_else.py
# content of tests/test_something_else.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-else-' + username
def test_username(username):
assert username == 'overridden-else-username'
在上面的示例中,一个具有相同名称fixture可以为某些测试模块重写覆盖。
通过直接测试参数化覆盖夹具
假设测试文件结构为:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
test_something.py
# content of tests/test_something.py
import pytest
@pytest.mark.parametrize('username', ['directly-overridden-username'])
def test_username(username):
assert username == 'directly-overridden-username'
@pytest.mark.parametrize('username', ['directly-overridden-username-other'])
def test_username_other(other_username):
assert other_username == 'other-directly-overridden-username-other'
在上面的示例中,fixture值被测试参数值覆盖。注意,即使测试没有直接使用fixture的值(在函数原型中没有提到),也可以用这种方式覆盖fixture的值。
用非参数化固件替代参数化固件,反之亦然。
假设测试文件结构为:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture(params=['one', 'two', 'three'])
def parametrized_username(request):
return request.param
@pytest.fixture
def non_parametrized_username(request):
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def parametrized_username():
return 'overridden-username'
@pytest.fixture(params=['one', 'two', 'three'])
def non_parametrized_username(request):
return request.param
def test_username(parametrized_username):
assert parametrized_username == 'overridden-username'
def test_parametrized_username(non_parametrized_username):
assert non_parametrized_username in ['one', 'two', 'three']
test_something_else.py
# content of tests/test_something_else.py
def test_username(parametrized_username):
assert parametrized_username in ['one', 'two', 'three']
def test_username(non_parametrized_username):
assert non_parametrized_username == 'username'
在上面的示例中,参数化固件被非参数化版本覆盖,而非参数化固件被某些测试模块的参数化版本覆盖。显然,这同样适用于测试文件夹级别。