软件测试夹具是用于初始化的测试功能的。它们提供了一个固定的基线,以便测试能够可靠地执行并产生一致的,可重复的结果。初始化可能会设置服务、状态或其他运行环境。测试函数可以通过参数访问测试夹具,通常在测试函数的定义中对于测试函数使用的每一个夹具都有定义。pytest 夹具对经典的xUnit风格的setup/teardown函数进行了显著的改进
另外,pytest还会继续支持传统xunit风格的setup。你可以按照你的喜好混合两种风格,渐渐的从传统的风格切换到新风格。你还可以从一个已经存在的unnttest.TestCase风格或nose风格的项目开始。
译者注:nose是一个继承自unittest的测试库,比unittest更好用。
夹具使用 @pytest.fixture 注解定义下面的夹具。pytest有十分有用的内置夹具,列在下面供参考:
测试函数可以通过在输入参数中填入夹具的名字来获取夹具对象。对于每个参数名,对应的夹具会提供对应的夹具对象。夹具函数的定义是通过给函数添加标记 @pytest.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-6.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 =
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_smtpsimple.py:14: AssertionError
========================= short test summary info ==========================
FAILED test_smtpsimple.py::test_ehlo - assert 0
============================ 1 failed in 0.12s =============================
在失败的回溯中我们看到smtplib中的smtp_connection作为测试函数被调用时的参数。SMTP()对象是在夹具中被创建的。测试方法失败的原因是我们故意设置的assert 0.下面描述的是这样使用夹具的时候pytest的具体行为:
注意如果你错误的拼写了你想用的夹具名或使用一个不可用的夹具,你会看见一个有所有可用夹具名字列表的错误。
注意,你可以使用下面的代码来查看可用的夹具:
pytest --fixtures test_simplefactory.py
只有使用了-v参数,以下划线开头的夹具才会被显示出来。
fixture允许测试函数轻松地接收和处理特定的预先初始化的应用程序对象,而不必关心import/setup/cleanup的细节。一个关于依赖注入常见的使用是夹具函数作为注入项而测试函数作为夹具对象的消费者。
在测试实现的时候如果你发现一个夹具要在多个测试文件中使用,你可以将其移动到 conftest.py 文件中。你在引用一个夹具的时候,不需要import夹具所在的文件,夹具会自动的被pytest发现。夹具发现的顺序是 测试类 - 测试模块 - conftest.py - 内置夹具 - 第三方插件夹具。
你也可以使用 conftest.py 文件去实现 local per-directory plugins(每个目录自己的插件).
译者注:最后一段的意思是,conftest.py文件不仅为夹具提供了一个共享的位置,还可以通过在这个文件里面实现钩子,来达到针对某一个目录进行一些操作的目的,比如在某一个目录的conftest文件中实现pytest_runtest_setup钩子,为这个目录中的每一个测试用例都打印一句话
如果你希望从文件中创建一些对于测试可用的测试数据,一个好办法是把这些数据加载进夹具中来使用。这里利用了pytest的自动缓存机制。
另一个好办法是把数据添加到tests文件夹中。有一些社区插件可以帮助我们管理这些测试数据,例如pytest-datadir 和 pytest-datafiles.
需要网络连接的夹具依赖于网络连接的顺畅,通常都需要花费大量时间。扩展前面的例子,我们可以为@pytest.fixture修饰的夹具添加scope="module"参数,来使得修饰的 smtp_connection 夹具在每个模块仅触发一次(默认每次测试函数触发一次)。在一个模块中的多个测试函数会因此获得相同的smtp_connection夹具实例,从而节省时间。scope可以填写的值有:function, class, module, package or session.
下面的例子把夹具放到了一个单独的conftest.py文件中,所以在同一个目录的多个模块都可以访问这个夹具:
# 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,你可以通过在测试函数中添加这个名字的参数来访问夹具(测试函数所在的文件应该在这个目录中或在这个目录的下级目录中)。
# 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-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
previous page)
rootdir: $REGENDOC_TMPDIR
collected 2 items
test_module.py FF [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection =
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:7: AssertionError
________________________________ test_noop _________________________________
smtp_connection =
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:13: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
============================ 2 failed in 0.12s =============================
你看到两个assert 0断言失败,更重要的是你可以在pytest输出的traceback中看到两个相同的(模块范围的)smtp_connection对象被传入了测试函数中。结果是由于两个测试函数复用了相同的 smtp_connection 对象,所以这个例子的运行速度要快于单个运行每一个测试用例。
如果你发现你需要一个会话级别的smtp_connection 对象,你可以使用下面的定义:
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests needing it
...
夹具在第一次被调用的时候被创建,而其销毁时机是根据其范围决定的。
注意:pytest在同一时间只缓存一个夹具的一个实例,这就表明当我们使用参数化的夹具的时候,pytest会在指定范围内被多次调用。
5.2版本新增.
在一些情况下,你可能想要在不改变代码的情况下修改夹具的范围。你可以通过给scope传递一个可调用对象来实现它(译者注:这里的可调用对象,其实就是一个内置的函数,通过传递一个函数来修改范围值,具体参见下面的例子)。这个函数必须返回一个合法的范围名,这个函数会在夹具定义的时候被执行一次。这个函数必须有两个参数:fixture_name和config.
这个特性在编写例如大量启动docker容器这种需要花费大量时间的夹具的时候非常有用,你可以使用命令行来控制在不同环境中启动容器的范围。
看下面的例子:
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers", None):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
对于夹具函数,那些高范围的(例如会话级别的)会比低级别的(例如function或class级别的)更早的被实例化。那些在同一个范围的夹具他们的顺序是依据定义的顺序,夹具之间是高度独立的。自动应用的夹具会比非自动应用的夹具更早的实例化。
考虑下面的代码:
import pytest
# fixtures documentation order example
order = []
@pytest.fixture(scope="session")
def s1():
order.append("s1")
@pytest.fixture(scope="module")
def m1():
order.append("m1")
@pytest.fixture
def f1(f3):
order.append("f1")
@pytest.fixture
def f3():
order.append("f3")
@pytest.fixture(autouse=True)
def a1():
order.append("a1")
@pytest.fixture
def f2():
order.append("f2")
def test_order(f1, m1, f2, s1):
# 不管声明的顺序如何,范围更大的一定先调用,所以:
# 首先调用的一定是s1 m1
# 在function范围,最先调用的一定是autouse,所以下一个是a1
# 下一个本应是f1,但是它用了f3,所以先f3 再f1
# 最后是f2
assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]
test_order的夹具会以下面的顺序被实例化:
pytest支持当夹具超出作用域时,执行特定夹具的清理代码。可以通过使用yield替换return,可以实现teardown的功能,所有在yield之后的代码将会执行清理工作。
# 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
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
2 failed in 0.12s
我们看到,smtp_connection对象在两个测试执行完成之后销毁。注意如果我们用 scope=‘function’ 来修饰夹具,清理工作会在每一个单个的测试之后进行。不管哪种情况,测试本身都不需要修改或者了解夹具的细节信息。
注意我们可以无缝的结合with来使用yield:
# 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会在测试结束之后自动的关闭,因为在with结束的时候,smtp_connection就被自动的关闭了。
使用 contextlib.ExitStack 来管理夹具的清理可以保证在setup代码发生异常的情况下,清理代码仍然能被执行。这种技术可以保证一个夹具创建或者获取资源,但是失败的时候,资源仍然能够被正确释放。
# content of test_yield3.py
import contextlib
import pytest
@contextlib.contextmanager
def connect(port):
... # create connection
yield
... # close connection
@pytest.fixture
def equipments():
with contextlib.ExitStack() as stack:
yield [stack.enter_context(connect(port)) for port in ("C1", "C3", "C28")]
在上面的例子中,如果C28因为异常而失败,C1 和 C3仍然能够正确的关闭。
注意如果在setup的过程中(yield关键字之前)发生异常,teardown的代码就不会运行。
另一个执行teardown代码的可选方法是利用上下文request对象的addfinalizer方法来注册一个结束方法。
下面是 smtp_connection 夹具使用 addfininalizer进行清理的方法:
# 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
下面是equipments夹具使用addfininalizer进行清理的例子:
# content of test_yield3.py
import contextlib
import functools
import pytest
@contextlib.contextmanager
def connect(port):
... # create connection
yield
...# close connection
@pytest.fixture
def equipments(request):
r = []
for port in ("C1", "C3", "C28"):
cm = connect(port)
equip = cm.__enter__()
request.addfinalizer(functools.partial(cm.__exit__, None, None, None))
r.append(equip)
return r
yield和addfinalizer方法都会在测试执行完成之后运行。当然,如果在清理函数注册之前发生异常的话,清理函数就不会运行了。
夹具可以接收一个request对象去内置“requesting”测试方法,类,或者模块的上下文。进一步扩展我们之前的smtp_connection夹具的例子,我们来使用我们的夹具从测试模块中读取一个server 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 {} ({})".format(smtp_connection, server))
smtp_connection.close()
我们使用 request.module 属性去从测试模块中获得一个可选的 smtpserver 属性。如果我们仅仅执行它,没什么会改变:
$ pytest -s -q --tb=no
FFfinalizing (smtp.gmail.com)
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
2 failed in 0.12s
让我们快速的创建另一个测试模块,并且在模块的命名空间中设置服务器的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:6: in test_showhelo
assert 0, smtp_connection.helo()
E AssertionError: (250, b'mail.python.org')
E assert 0
------------------------- Captured stdout teardown -------------------------
finalizing (mail.python.org)
========================= short test summary info ==========================
FAILED test_anothersmtp.py::test_showhelo - AssertionError: (250, b'mail....
瞧!smtp_connection夹具函数使用了我们模块命名空间中的邮件地址。
译者注:说了这么半天,简单说就是提供一个可以往夹具中传值的方法,用于公用一些公共方法,但是又兼顾每个模块自己的个性定义.这里注意,python的模块是一个py文件,package是一个文件夹,不要搞混了
使用request对象,夹具也可以使用request对象访问测试函数的标记(marks)。这招在从测试函数向夹具中传递数据的时候十分有用:
import pytest
@pytest.fixture
def fixt(request):
marker = request.node.get_closest_marker("fixt_data")
if marker is None:
# Handle missing marker in some way...
data = None
else:
data = marker.args[0]
# Do something with the data
return data
@pytest.mark.fixt_data(42)
def test_fixt(fixt):
assert fixt == 42
“夹具工厂”模式在一个夹具的结果需要在测试中多次使用的场景中十分有用。我们不直接返回一个数据,而是返回一个生成数据的方法。这个方法可以在测试中被多次调用。可以按照需要向工厂中传递方法:
@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")
夹具函数可以在一个测试需要被多次调用的时候参数化的独立运行每一个用例,这种情况,测试会依赖于夹具。测试函数通常不需要意识到自己被重复执行了。参数化的夹具对于编写那些可以用多种方式配置的,用来编写详尽的测试组件的时候十分有用。
扩展前面的例子,我们可以标记夹具 smtp_connection 对象,这会导致所有使用这个夹具的对象运行两次。夹具方法通过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 {}".format(smtp_connection))
smtp_connection.close()
在使用 @pytest.fixture 声明夹具时候的主要区别是,我们可以通过在夹具中访问request.param来访问声明中一系列的值,测试函数本身的代码无需改变,让我们运行一下:
$ pytest -q test_module.py
FFFF [100%]
================================= FAILURES =================================
________________________ test_ehlo[smtp.gmail.com] _________________________
smtp_connection =
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:7: AssertionError
________________________ test_noop[smtp.gmail.com] _________________________
smtp_connection =
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:13: AssertionError
________________________ test_ehlo[mail.python.org] ________________________
smtp_connection =
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:6: AssertionError
-------------------------- Captured stdout setup ---------------------------
finalizing
________________________ test_noop[mail.python.org] ________________________
smtp_connection =
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:13: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo[smtp.gmail.com] - assert 0
FAILED test_module.py::test_noop[smtp.gmail.com] - assert 0
FAILED test_module.py::test_ehlo[mail.python.org] - AssertionError: asser...
FAILED test_module.py::test_noop[mail.python.org] - assert 0
4 failed in 0.12s
我们看到我们两个测试每一个都使用不同的smtp_connection对象运行了两次。同时注意,连接 mail.python.org 的第二个测试失败的原因是收到的服务字符串与期望的不一致。
pytest会为参数化夹具的每一个参数创建一个字符串类型的 测试ID,例如在上面的例子中:test_ehlo[smtp.gmail.com] 和 test_ehlo[mail.python.org]。 这些ID可以使用-k参数运行指定的用例,ID的另一个用处是在测试失败的时候标明是哪一个具体的测试。使用 --collect-only 参数运行,会显示生成的ID。
Numbers, strings, booleans 和 None 类型的参数,pytest会使用这些类型的常见字符串表达。对于其他的对象,pytest会基于参数名称生成一个字符串。我们也可以通过 ids 键值,给某一个夹具参数值自定义一个测试ID:
# 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参数的方法,传入的函数参数是 参数化夹具的参数值,返回一个可用的ID,如果这个函数返回None,pytest的内置生成ID将会被启用。运行上面的测试,这些ID会被用到:
译者注:如果在参数中使用None参数,ID就是None,但是如果在函数中返回None,则使用内置的生成ID,一般的逻辑是使用参数值作为ID
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 10 items
========================== 10 tests found in 0.12s ===========================
使用 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
运行这个test将会跳过参数2的测试:
$ pytest test_fixture_marks.py -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.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.12s =======================
译者注:一套代码,多个数据驱动多个测试,这就是测试驱动的理念,在这一节,我们看到了在夹具侧,我们可以让所有使用该夹具的测试都使用同一套数据驱动,在测试侧,我们也可以使用 @pytest.mark.parametrize 来进行数据驱动,不管用哪种方法,都可以使用 pytest.param(2, marks=pytest.mark.skip) 这种方式来给某一个数据驱动的测试添加一个mark
除了在测试函数中使用夹具之外,夹具函数本身也可以使用其他夹具。这对于夹具进行模块化设计十分有用,允许在多个项目中重用夹具。看一个
简单的例子,我们可以扩展前面的例子,并实例化一个app对象,我们把已经定义的 Smtp_connection 资源添加到其中:
# content of test_appsetup.py
import pytest
class App:
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
现在我们声明了一个参数是我们之前 smtp_connection 夹具的一个夹具,并且实例化了App对象。我们运行它:
$ pytest -v test_appsetup.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.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.12s =============================
由于smtp_connection是参数化的,测试会使用两个不同的APP对象来运行两次。app夹具无需关心smtp_connection夹具的参数化情况,pytest会全面的分析夹具的依赖情况。
注意,app夹具的范围是 module,它使用了同样是 module 范围的夹具 smtp_connection夹具。如果 smtp_connection 是session范围,例子也可以正常运行,一个夹具要使用另一个夹具,应该使用范围比自己更广的夹具,但是翻过来就不行,使用一个范围比自己小的夹具是没有意义的。
译者注:我们之前就讲过夹具之前可以嵌套,这里我们知道了嵌套夹具的时候不能嵌套范围比自己小的夹具
pytest在测试执行的时候会保留最少的夹具对象。如果你有一个参数化夹具,则所有使用这个夹具的测试会使用同一个夹具对象,然后在下一个夹具产生之前调用终结器销毁。另一方面,这样能够更方便的创建和使用全局状态:
下面的例子使用了两个参数化的夹具,其中一个是module级别的,所有的函数都打印了调用,以显示 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", param)
yield param
print(" TEARDOWN modarg", param)
@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
param = request.param
print(" SETUP otherarg", param)
yield param
print(" TEARDOWN otherarg", param)
def test_0(otherarg):
print(" RUN test0 with otherarg", otherarg)
def test_1(modarg):
print(" RUN test1 with modarg", modarg)
def test_2(otherarg, modarg):
print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))
我们使用-v参数运行这个测试,然后观察输出:
$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.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.12s =============================
你可以看到使用 module 范围的参数化夹具 modarg之后,测试的执行顺序遵从“最小活动资源”原则。在mod2 执行setup之前,mod1的结束器被调用了。
特别要说明, test_0是完全独立的,也是第一个结束的。之后test_1被执行,它的夹具是mod1,之后是使用mod1夹具的test_2,之后是使用mod2的test_1,最后是使用mod2的test_2。
函数范围的 otherarg ,在使用之前set up 使用之后就tear down.
有些时候,测试函数不需要直接访问夹具对象。例如,测试的执行可能需要在工程目录下建立一个空的目录,但是又不需要关心具体是哪个目录。这里我们看看如何使用标准的 tempfile 和 pytest的夹具来实现它。我们把夹具的创建拆分到一个 conftest.py 文件中去:
# content of conftest.py
import os
import shutil
import tempfile
import pytest
@pytest.fixture
def cleandir():
old_cwd = os.getcwd()
newpath = tempfile.mkdtemp()
os.chdir(newpath)
yield
os.chdir(old_cwd)
shutil.rmtree(newpath)
然后在一个模块中使用usefixture 标记来使用这个夹具:
# content of test_setenv.py
import os
import pytest
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
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 夹具会被每个测试方法执行,就跟你给每个测试方法都使用 cleandir 作为参数的效果是一样的。我们可以执行一下去验证我们的夹具是不是执行了:
译者注:注意夹具的默认执行范围是function,所以这里虽然userfixture是标在class上面的,但是仍然每个方法都会执行
$ pytest -q
.. [100%]
2 passed in 0.12s
你可以像这样标记使用多个夹具:
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
...
你还可以指定在测试模块的级别使用pytestmark指定夹具:
pytestmark = pytest.mark.usefixtures("cleandir")
译者注:之前的例子是usefixture来修饰一个类,这个例子是在一个模块中,即使没有类,把上面一行代码加入到一个模块的代码之中,pytestmark无需调用,夹具就会自动的被所有测试函数使用,但是注意: pytestmark 这个变量值不能随意指定,必须是 pytestmark
通过在ini配置文件中指定使用一个全局的夹具也是可以的:
# content of pytest.ini
[pytest]
usefixtures = cleandir
警告:这个标记不能在编写一个夹具的时候使用。例如下面的例子不会按照预想的运行:
@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture():
...
目前这不会生成任何的错误或者警告,但是这个问题计划将被 #3664 解决。
译者注: #3664是Github的issues
我们有时候可能希望一个夹具自动的被插入到我们的测试中,不需要在函数参数中指定,也不需要 usefixtures 来修饰。举一个实际的例子,假设我们有一个管理数据库操作的夹具,拥有 开始/提交/回滚这样的操作,我们希望在每一个测试方法周围都围绕上这些传输/回滚这样的操作,我们可以虚拟的实现一下这个想法:
# content of test_db_transact.py
import pytest
class DB:
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:
@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"]
这个class级别的transact夹具被标记为 autouse=true,这意味着在这个类中所有的测试都无需在参数中指定或者使用类级别的 usefixtures 来修饰。
如果我们运行这个程序,我们将得到两个通过的测试:
$ pytest -q
.. [100%]
2 passed in 0.12s
下面是在其他作用域下autouse夹具如何生效的说明:
# content of conftest.py
@pytest.fixture
def transact(request, db):
db.begin()
yield
db.rollback()
在类 TestClass 需要使用的时候,指定的使用夹具:
@pytest.mark.usefixtures("transact")
class TestClass:
def test_method1(self):
...
TestClass中的所有测试都会使用 transaction夹具,其他类或模块中的函数则不会使用这个夹具,除非他们也显式的指明要使用夹具。
在相对比较大的测试集中,你可能需要使用本地的定义复写一些全局的夹具来保持测试代码的可读性和可维护性。
给定的测试文件的结构为:
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'
就像你看到的,同一个名字的夹具会被下级的夹具覆盖,注意,上级的夹具能够非常轻易的被下级夹具调用到,就像上面的例子中展示的那样。
给定的测试程序结构为:
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
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'
根据上面的例子,我们可以在module的层面使用相同的夹具名字覆盖更高层级的夹具的行为。
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'
在上面的例子中,我们使用参数值复写了夹具的返回值。注意,即使是没有直接使用的夹具也可以被复写,就像上面最后的一个测试一样。
给定的例子如下:
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'
在上面的例子中,一个参数化的夹具可以被不是参数化的夹具复写,反之亦然。上面的例子是在module层面的复写,如果是在文件夹层面的复写也是有效的。
通常,使用pytest的项目都支持使用入口点(entry point),我们可以把项目安装到环境中,可以让项目中的夹具被共享出来。
万一你想用的项目没有对外暴露入口点,你可以在conftest.py中把这个模块注册为插件。
假设你有很多夹具在mylibrary.fixtures中,你希望在 app/tests中复用夹具。
你需要做的就是在app/tests/conftest.py 定义pytest_plugins(插件)纸箱需要调用的模块。
pytest_plugins = "mylibrary.fixtures"
这可以有效的将mylibrary.fixtures 作为一个插件,使得所有的钩子和夹具在app/tests中可用。
注意:一些时候用户会在其他项目中引入夹具以供使用,但是这并不被推荐:在模块中引入夹具会使得这些夹具注册进模块中。这会有一些小的后果,比如使用 pytest --help 命令的时候夹具会重复出现,但是这么做仍然不被推荐,因为这个特性可能会在将来的版本中被改变。