Chapter 5
pytest fixtures: explicit, modular, scalable
pytest fixtures: 明确、模块化、可伸缩
翻译水平低,只是方便自己以后查看
测试夹具Fixtures
的目的,是为了让测试可以可靠地重复执行提供一个基准。pytest fixtures
在传统的xUnit风格的 setup/teardown
函数基础上提出了显著的提高:
- fixtures 具有显示的名字,并通过从函数、模块、类或整改项目中声明它们的使用来激活它们。
- fixtures 以模块化的方式实现,因为每个fixture名称都触发一个fixture函数,该函数可以使用其他fixture。
- fixture 可用的范围可以从简单的单元测试到复杂的功能测试,允许根据配置和组件选项参数化fixtures和测试,或者跨函数、类、模块或整个测试session范围内重用fixture。
此外,pytest 继续支持传统的xUnit风格的setup
,你可以混可两种风格,以增量的方式迁移到新的风格,随你喜欢。你也可以从已经存在的unittest.TestCase
风格或nose based
项目开始。
5.1 以函数参数形式的fixture
测试函数可以将fixture对象命名为输入参数来接收他们。对于每一个参数名,具有该名称的fixture函数提供fixture对象。fixture函数通过标记@pytest.fixture
来注册。我们来看看一个简单的自带测试代码的函数模块,其中含有一个fixture和一个使用它的测试函数:
# content of ./test_smtpsimple.py
import pytest
import smtplib
@pytest.fixture
def smtp_connection():
return smtplib.SMTP('smtp.qq.com', 587, timout=5)
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert 0 # for demo purposes
这里test_ehlo
需要smtp_connection
fixture的值。pytest将发现并调用被@pytest.fixture
标记的smtp_connection
fixtrue 函数。运行这个测试看起来像这样:
$ 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
参数调用了,smtplib.SMTP()
实例被fixture函数创建。测试函数失败在故意设置的assert 0
上。下面是pytest调用测试函数使用的具体协议:
- 因为
test_
前缀,pytest 找到test_ehlo
。测试函数需要一个名为smtp_connection
的函数参数。查找一个被fixtrue标记的名为smtp_connection
的函数,发现了一个匹配的fixture函数。 -
smtp_connection()
被调用来创建一个实例。 -
test_ehlo(<smtp_connection instance>)
被调用并失败在测试函数的最后一行
Note:如果你写错了一个函数参数或者想使用的不可用,你将看到一个含有可用函数参数的列表的错误
你通常可以执行下列代码查看可用的fixtures(以_开头的fixtures只有在你加上-v
选项的时候才会显示)。
pytest --fixtures test_simplefactory.py
5.2 Fixtures: 一个依赖注入的典型例子
Fixtures 允许测试函数轻易地接收并处理特定的预初始化应用程序对象,不需要关心 import
setup
cleanup
的细节。这是依赖注入的一个典型例子,其中fixture函数充当注入器的角色,而测试函数则是fixture对象的消费者。
5.3 conftest.py
共用fixture函数
如果在实现测试的过程中你意识到想要在多个测试文件中使用同一个fuxture函数,你可以将其移动到conftest.py
文件中。你无需import这个fixture,pytest会自动找到它。fixture函数的发现从测试类开始,然后是测试模块,然后是conftest.py
文件和内置、第三方插件。
你也可以使用conftest.py
文件实现每个目录的本地插件。
5.4 共用测试数据
如果你想让你的测试可以使用文件中的测试数据,一个好方式是,通过加载这些数据到一个被测试使用的fixture里。这利用到了pytest的自动缓存机制。
5.5 Scope
:在类、模块或会话中共用一个fixture实例
需要网络访问的fixtures依赖于连接性,通常创建这些非常耗时。扩展一下前面的实例,我们可以向@pytest.fixture
添加一个scope='module'
参数,以使每个测试模块只调用一次被@pytest.fixture
修饰的smtp_connection
fixture函数。在一个测试模块里得多个测试函数将接受相同的smtp_connection
fixture实例,从而节省时间。scope
参数接受的值可以是:function
class
module
package
or session
. function
是缺省值。
下面的例子是将fixture函数放在了一个单独的conftest.py
文件中,所以在同一个路径下的测试模块都可以访问这个fixture函数:
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope='module'):
def smtp_connection():
return smtplib.SMTP('smtp.qq.com', 587, timeout=5)
这个fixture的名称也是smtp_connection
,你可以在任何conftest.py
所在目录或目录下的测试或fixture中通过列出名称smtp_connection
作为入参的形式来访问它的结果。
# content of test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehclo()
assert response == 250
assert b'smtp.qq.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
...
最终,这个类的作用域的每个测试类将调用这个fixture一次。
Note: pytest一次只缓存一个fixture实例。这意味着当使用一个参数化的fixture时,pytest可以在指定的作用域内多次调用一个fixture。
5.5.1 package
作用域(实验中)
在pytest 3.7中,引入了package
作用域。包作用域的fixture在一个包的测试都结束后完成。
警告:该功能还在实验中,如果在更多的使用中发现了严重的问题,可能在未来的版本中会被删除。请谨慎使用此功能,并请务必向我们报告您发现的任何问题。
5.6 更高作用域的fixture会被首先实例化
在功能请求的特性中,更高作用域的fixture(例如session
)比低作用域的fixture(例如function
或class
)更先实例化。同一作用域的fixture的相对顺序,依照在测试函数中的声明顺序和fixtures之间的依赖关系。
参看如下代码:
@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
请求的fixtures依照以下顺序实例化:
- s1:是最高作用域的fixture(session)
- m1:是第二高作用域的fixture(module)
- tmpdir:是一个function作用域的fixture,被f1请求,需要被实例化,因为他是f1的依赖项
- f1:是
test_foo
参数列表中的第一个function作用域的fixture - f2:是
test_foo
参数列表中的最后一个function作用域的fixture
5.7 fixture结束/执行teardown
代码
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.qq.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
实例在两个测试运行完成的时候结束了。
Note:如果我们用
scope='function'
装饰fixture函数,fixture的setup和cleanup会在每一个测试执行。测试模块的任一个用例都不用修改或者知道fixture的setup。
Note:我们同样可以将yield
与with
类似地使用:
# content of test_yield2.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
with smtplib.SMTP("smtp.qq.com", 587, timeout=5) as smtp_connection:
yield smtp_connection # provide the fixture value
smtp_connection
链接将会在测试结束运行后被关闭,因为smtp_connection
对象在with
结束后自动关闭。
Note:如果在
setup
代码期间(yield
关键字之前的代码)发生了异常,teardown
代码(yield
关键字之后的代码)将不会被调用。
一个可供替代的运行teardown
代码选项是使用request-context
对象的addfinalizer
方法来注册结束函数。
这里是smtp_commection
fixture修改为使用addfinalizer
作为cleanup
:
# 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
方法工作方式都类似,都是在测试结束后调用他们的代码,但是addfinalzer
相较于yield
有两个不同的点:
- 可能有多个结束方法。
- 如果fixture
setup
代码发生了异常,结束代码依然会被调用。即使setup
代码发生了再多的创建失败/获取失败,也能适时地关闭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
将适时地关闭。当然,如果在注册结束函数前发生异常,它就不会运行。
5.8 fixture 可以对请求它的测试内容进行自省(反向获取测试函数的环境)
fixture函数可以接受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.qq.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.qq.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函数从模块的命名空间里获取了我们的邮箱服务器名字。
5.9 将fixture作为工厂
factory as fixture
模式,可以在“单个测试中多次需要fixture”的情况下提供帮助。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")
如果工厂创建的数据需要管理,fixture可以处理:
@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")
5.10 参数化fixtures
fixture函数可以在被参数化,参数化后被多次调用,每次执行依赖测试集合,相当于这些测试依赖于这个fixture。测试函数通常不会需要知道他们的重新运行。fuxture参数有助于为组件编写详细的功能测试,组件本身可以通过多种方式配置。
扩展之前的例子,我们可以标记fixture来创建两个smtp_connection
fixture实例,这将导致所有的测试使用rixture运行两次。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()
主要的变化是带有@pytest.fixture
声明的praram
,它是一组值,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.qq.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.qq.com" in msg
E AssertionError: assert b'smtp.qq.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
实例。
Note: 对于
mail.python.org
,test_ehlo
的第二次失败是因为预期的服务器字符串与最终的不同。
pytest将构建一个字符串,这个字符串是参数化fixture中的每个值得测试ID,比如在上面例子中的test_ehlo[smtp.gmail.com]
和test_ehlo[mail.python.org]
。这些ID可以和-k
一起使用,以旋转要运行的特定cases,当一个case失败时,他们还将标记特定的cases。使用--collect-only
运行pytest将显示生成的ID。
Numbers
, strings
, booleans
and None
将在测试ID中显示他们通常的字符串表示形式。对于其他的对象,pytest将做一个基于参数名字的字符串。可以使用ids
关键字参数为某个fixture值定制测试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
可以是要使用的字符串列表,也可以是使用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 =======================
5.11 使用参数化标记
pytest.param()
可以在参数化fixtrue的值集中应用标记,方法与@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
运行这个测试将会跳过值为2的data_set
声明:
$ 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 ====================
5.12 模块化:使用fixture函数中的fixtures
你不仅仅可以在测试函数中使用fixtures,还可以使用fixture函数可以使用其他的fixture。这有助于fixture的模块化设计,并允许跨许多项目重用特定框架的fixture。作为一个简单的例子,我们可以扩展之前的例子并实例化一个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
fixture,它会接收之前定义的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
fixture不需要关注smtp_connection
参数化,因为pytest将全面分析fixture依赖关系图。
Note:
app
fixture的作用域是模块,且使用了一个模块作用域的smtp_connection
fixture。如果smtp_connection
的缓存在session
作用域,这个示例仍然可以正常工作:fixture可以使用“更广泛的”作用域fixture,但是反过来不行:session
作用域的fixture不能以有意义的方式使用作用域为module
的fixture。
5.13 按照fixture实例自动将测试分组
pytest 最小化了再测试运行期间活动fixture的数量。如果你有参数化的fixture,所有使用了它的测试将首先使用一个实例执行,然后再下一个fixture实例创建之前调用终结器。除此之外,这还简化了对创建和使用全局状态的应用程序的测试。
接下来的例子,使用了两个参数化的fixtrue,一个作用域是每个模块,所有的函数执行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_REFIX/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
参数化资源(拥有函数作用域)在每一个使用它的测试之前set up
并在其之后tear down
。
5.14 从class、module或者project中引用fixture
有时,测试函数不需要直接访问fixture对象。例如,测试可能会操作一个空的目录作为当前工作目录进行操作,同时又不关心具体是什么目录。下面是如何使用标准的tempfile
和pytest fixture来实现它。我们将fixture的创建分离到conftest.py
文件中:
# content of conftest.py
import pytest
import tempfile
import os
@pytest.fixture()
def cleandir():
newpath = tempfile.mkdtemp()
os.chdir(newpath)
并且通过usefixture
标记在测试模块中声明它的使用:
# 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
标记,每个测试方法的运行都需要请求cleanfir
fixture,就好像你为他们每个指定了一个cleandir
函数参数一样。让我们运行一下来严重我们的fixture是激活并测试通过了:
$ pytest -q
.. [100%]
2 passed in 0.12 seconds
你可以像这样指定多个fixture:
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
...
你可以使用mark机制的通用特性,在测试模块级别指定fixture的使用方式:
pytestmark = pytest.mark.usefixture('cleandir')
Note: 指定的变量必须名为
pytestmark
,例如foomark
将不会激活fixture。也可以将项目中所有测试所需的fixture放入一个ini文件中:
# content of pytest.ini
[pytest]
usefixtures = cleandir
Warning:注意这里的标记对fixture函数没有效果。例如,这将不会像预期的那样工作:
@pytest.mark.usefixtures("my_other_fixture") @pytest.fixture def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
目前,这不会生产任何错误或警告,但是这将由#3664处理。
5.15 自动调用fixture(xUnit setup on steroids)
有时候,你可能想在不显示声明函数参数或usefixtures
装饰器的情况下自动调用fixture。作为一个世纪的例子,假设我们有一个数据库fixture,它具有一个begin/rollback/commit
体系结构,并且我们希望通过事务和回滚自动地包裹每个测试方法。这里是这个想法的虚拟的自包含实现:
# 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"]
class级别的transact
fixture被标记了autouse=true
,意味着类中的所有测试方法都将使用这个fixture,二不需要在测试函数签名中声明它,也不需要使用类级别的usefixture
装饰器。
如果我们运行它,会得到两个通过的测试:
$ pytest -q
.. [100%]
2 passed in 0.12 seconds
这里是在其他作用域 autouse的fixture怎么工作
- autouse fixture遵循
scope=
参数:如果一个autouse fixture具有scope='session'
,那么他讲值运行一次,无论它在哪里定义的。scope='class'
表示它将在每个类中运行一次,等等。 - 如果在测试模块中定义了一个autouse fixture,那么这个模块下的所有测试函数都会自动使用它。
- 如果在
conftest.py
文件中定义了一个autouse fuxture,那么在同一个目录下所有测试模块中的所有测试都会调用该fixture - 最后,使用的时候请小心:如果在插件中定义了一个autouse fixture,那么它将被所有安装了插件的项目中的所有测试使用。如果fixture只是在某些(例如在ini文件中)设置存在的情况下工作,那么它将非常有用。这样的全局fixture应该总是快速递确定它是否应该执行任何工作,避免了其他昂贵的导入或计算。
请注意,上面的transaction
fixture很可能是希望你在项目中使用的fixture,而不需要它处于激活状态。规范的方法是将这个事务定义在一个conftest.py
文件中,而不是使用autouse
:
# content of conftest.py
@pytest.fixture
def transact(request, db):
db.begin()
yield
db.rollback()
并且例如,有一个TestClass使用它来声明需要使用这个fixture:
@pytest.mark.usefixtures("transact")
class TestClass(object):
def test_method1(self):
...
所有在TestClass里的测试方法将会使用transaction
fixture,而模块中的其他测试类或函数将不会使用它,除非它们也添加了transact
引用。
5.16 覆盖不同级别的fixture
在相对较大的测试套件中,你极大可能需要用本地定义的fixture覆盖全局或根fixture,以保持测试代码的可读性和可维护性。
5.16.1覆盖文件夹(conftest.py)级别的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可以被子文件夹中的fixture覆盖。请注意,在上面的例子中,可以从overriding
fixture轻易地访问base
或者super
fixture。
5.16.2 在测试模块级别覆盖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可以被某些测试模块覆盖。
5.16.3 直接用测试的参数化覆盖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的值(在函数原型中没有提到),也可以用这种方式覆盖。
5.16.4 用非参数化的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'
在上面的例子中,参数化的fixture被非参数化版本覆盖,而非参数化fixture被某些测试模块的参数化版本覆盖。显然,测试文件夹级别也是如此。
【第5章 完】