introduce yieldctx=True in the @pytest.fixture decorator. Refactor tests and docs.

This commit is contained in:
holger krekel 2013-09-26 12:57:21 +02:00
parent 2bdd034242
commit 3ab9b48782
3 changed files with 198 additions and 129 deletions

View File

@ -26,19 +26,21 @@ def getimfunc(func):
class FixtureFunctionMarker: class FixtureFunctionMarker:
def __init__(self, scope, params, autouse=False): def __init__(self, scope, params, autouse=False, yieldctx=False):
self.scope = scope self.scope = scope
self.params = params self.params = params
self.autouse = autouse self.autouse = autouse
self.yieldctx = yieldctx
def __call__(self, function): def __call__(self, function):
if inspect.isclass(function): if inspect.isclass(function):
raise ValueError("class fixtures not supported (may be in the future)") raise ValueError(
"class fixtures not supported (may be in the future)")
function._pytestfixturefunction = self function._pytestfixturefunction = self
return function return function
def fixture(scope="function", params=None, autouse=False): def fixture(scope="function", params=None, autouse=False, yieldctx=False):
""" (return a) decorator to mark a fixture factory function. """ (return a) decorator to mark a fixture factory function.
This decorator can be used (with or or without parameters) to define This decorator can be used (with or or without parameters) to define
@ -59,12 +61,17 @@ def fixture(scope="function", params=None, autouse=False):
:arg autouse: if True, the fixture func is activated for all tests that :arg autouse: if True, the fixture func is activated for all tests that
can see it. If False (the default) then an explicit can see it. If False (the default) then an explicit
reference is needed to activate the fixture. reference is needed to activate the fixture.
:arg yieldctx: if True, the fixture function yields a fixture value.
Code after such a ``yield`` statement is treated as
teardown code.
""" """
if callable(scope) and params is None and autouse == False: if callable(scope) and params is None and autouse == False:
# direct decoration # direct decoration
return FixtureFunctionMarker("function", params, autouse)(scope) return FixtureFunctionMarker(
"function", params, autouse, yieldctx)(scope)
else: else:
return FixtureFunctionMarker(scope, params, autouse=autouse) return FixtureFunctionMarker(scope, params, autouse, yieldctx)
defaultfuncargprefixmarker = fixture() defaultfuncargprefixmarker = fixture()
@ -1616,6 +1623,7 @@ class FixtureManager:
assert not name.startswith(self._argprefix) assert not name.startswith(self._argprefix)
fixturedef = FixtureDef(self, nodeid, name, obj, fixturedef = FixtureDef(self, nodeid, name, obj,
marker.scope, marker.params, marker.scope, marker.params,
marker.yieldctx,
unittest=unittest) unittest=unittest)
faclist = self._arg2fixturedefs.setdefault(name, []) faclist = self._arg2fixturedefs.setdefault(name, [])
if not fixturedef.has_location: if not fixturedef.has_location:
@ -1656,8 +1664,18 @@ class FixtureManager:
except ValueError: except ValueError:
pass pass
def call_fixture_func(fixturefunc, request, kwargs): def fail_fixturefunc(fixturefunc, msg):
if is_generator(fixturefunc): fs, lineno = getfslineno(fixturefunc)
location = "%s:%s" % (fs, lineno+1)
source = py.code.Source(fixturefunc)
pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location,
pytrace=False)
def call_fixture_func(fixturefunc, request, kwargs, yieldctx):
if yieldctx:
if not is_generator(fixturefunc):
fail_fixturefunc(fixturefunc,
msg="yieldctx=True requires yield statement")
iter = fixturefunc(**kwargs) iter = fixturefunc(**kwargs)
next = getattr(iter, "__next__", None) next = getattr(iter, "__next__", None)
if next is None: if next is None:
@ -1669,11 +1687,8 @@ def call_fixture_func(fixturefunc, request, kwargs):
except StopIteration: except StopIteration:
pass pass
else: else:
fs, lineno = getfslineno(fixturefunc) fail_fixturefunc(fixturefunc,
location = "%s:%s" % (fs, lineno+1) "fixture function has more than one 'yield'")
pytest.fail(
"fixture function %s has more than one 'yield': \n%s" %
(fixturefunc.__name__, location), pytrace=False)
request.addfinalizer(teardown) request.addfinalizer(teardown)
else: else:
res = fixturefunc(**kwargs) res = fixturefunc(**kwargs)
@ -1682,7 +1697,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
class FixtureDef: class FixtureDef:
""" A container for a factory definition. """ """ A container for a factory definition. """
def __init__(self, fixturemanager, baseid, argname, func, scope, params, def __init__(self, fixturemanager, baseid, argname, func, scope, params,
unittest=False): yieldctx, unittest=False):
self._fixturemanager = fixturemanager self._fixturemanager = fixturemanager
self.baseid = baseid or '' self.baseid = baseid or ''
self.has_location = baseid is not None self.has_location = baseid is not None
@ -1693,6 +1708,7 @@ class FixtureDef:
self.params = params self.params = params
startindex = unittest and 1 or None startindex = unittest and 1 or None
self.argnames = getfuncargnames(func, startindex=startindex) self.argnames = getfuncargnames(func, startindex=startindex)
self.yieldctx = yieldctx
self.unittest = unittest self.unittest = unittest
self._finalizer = [] self._finalizer = []
@ -1730,7 +1746,8 @@ class FixtureDef:
fixturefunc = fixturefunc.__get__(request.instance) fixturefunc = fixturefunc.__get__(request.instance)
except AttributeError: except AttributeError:
pass pass
result = call_fixture_func(fixturefunc, request, kwargs) result = call_fixture_func(fixturefunc, request, kwargs,
self.yieldctx)
assert not hasattr(self, "cached_result") assert not hasattr(self, "cached_result")
self.cached_result = result self.cached_result = result
return result return result

View File

@ -40,7 +40,7 @@ style <unittest.TestCase>` or :ref:`nose based <nosestyle>` projects.
.. _`@pytest.fixture`: .. _`@pytest.fixture`:
.. _`pytest.fixture`: .. _`pytest.fixture`:
Fixtures as Function arguments (funcargs) Fixtures as Function arguments
----------------------------------------- -----------------------------------------
Test functions can receive fixture objects by naming them as an input Test functions can receive fixture objects by naming them as an input
@ -70,7 +70,8 @@ marked ``smtp`` fixture function. Running the test looks like this::
$ py.test test_smtpsimple.py $ py.test test_smtpsimple.py
=========================== test session starts ============================ =========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.5 platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev12
plugins: xdist, pep8, cov, cache, capturelog, instafail
collected 1 items collected 1 items
test_smtpsimple.py F test_smtpsimple.py F
@ -78,7 +79,7 @@ marked ``smtp`` fixture function. Running the test looks like this::
================================= FAILURES ================================= ================================= FAILURES =================================
________________________________ test_ehlo _________________________________ ________________________________ test_ehlo _________________________________
smtp = <smtplib.SMTP instance at 0x226cc20> smtp = <smtplib.SMTP instance at 0x1ac66c8>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
@ -88,7 +89,7 @@ marked ``smtp`` fixture function. Running the test looks like this::
E assert 0 E assert 0
test_smtpsimple.py:12: AssertionError test_smtpsimple.py:12: AssertionError
========================= 1 failed in 0.20 seconds ========================= ========================= 1 failed in 0.17 seconds =========================
In the failure traceback we see that the test function was called with a In the failure traceback we see that the test function was called with a
``smtp`` argument, the ``smtplib.SMTP()`` instance created by the fixture ``smtp`` argument, the ``smtplib.SMTP()`` instance created by the fixture
@ -123,7 +124,7 @@ with a list of available function arguments.
but is not anymore advertised as the primary means of declaring fixture but is not anymore advertised as the primary means of declaring fixture
functions. functions.
Funcargs a prime example of dependency injection "Funcargs" a prime example of dependency injection
--------------------------------------------------- ---------------------------------------------------
When injecting fixtures to test functions, pytest-2.0 introduced the When injecting fixtures to test functions, pytest-2.0 introduced the
@ -142,7 +143,7 @@ functions take the role of the *injector* and test functions are the
.. _smtpshared: .. _smtpshared:
Working with a module-shared fixture Sharing a fixture across tests in a module (or class/session)
----------------------------------------------------------------- -----------------------------------------------------------------
.. regendoc:wipe .. regendoc:wipe
@ -188,7 +189,8 @@ inspect what is going on and can now run the tests::
$ py.test test_module.py $ py.test test_module.py
=========================== test session starts ============================ =========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.5 platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev12
plugins: xdist, pep8, cov, cache, capturelog, instafail
collected 2 items collected 2 items
test_module.py FF test_module.py FF
@ -196,7 +198,7 @@ inspect what is going on and can now run the tests::
================================= FAILURES ================================= ================================= FAILURES =================================
________________________________ test_ehlo _________________________________ ________________________________ test_ehlo _________________________________
smtp = <smtplib.SMTP instance at 0x18a6368> smtp = <smtplib.SMTP instance at 0x15b2d88>
def test_ehlo(smtp): def test_ehlo(smtp):
response = smtp.ehlo() response = smtp.ehlo()
@ -208,7 +210,7 @@ inspect what is going on and can now run the tests::
test_module.py:6: AssertionError test_module.py:6: AssertionError
________________________________ test_noop _________________________________ ________________________________ test_noop _________________________________
smtp = <smtplib.SMTP instance at 0x18a6368> smtp = <smtplib.SMTP instance at 0x15b2d88>
def test_noop(smtp): def test_noop(smtp):
response = smtp.noop() response = smtp.noop()
@ -217,7 +219,7 @@ inspect what is going on and can now run the tests::
E assert 0 E assert 0
test_module.py:11: AssertionError test_module.py:11: AssertionError
========================= 2 failed in 0.26 seconds ========================= ========================= 2 failed in 0.16 seconds =========================
You see the two ``assert 0`` failing and more importantly you can also see You see the two ``assert 0`` failing and more importantly you can also see
that the same (module-scoped) ``smtp`` object was passed into the two that the same (module-scoped) ``smtp`` object was passed into the two
@ -233,62 +235,17 @@ instance, you can simply declare it::
# the returned fixture value will be shared for # the returned fixture value will be shared for
# all tests needing it # all tests needing it
.. _`contextfixtures`: .. _`finalization`:
fixture finalization / teardowns fixture finalization / executing teardown code
------------------------------------------------------------- -------------------------------------------------------------
pytest supports two styles of fixture finalization: pytest supports execution of fixture specific finalization code
when the fixture goes out of scope. By accepting a ``request`` object
into your fixture function you can call its ``request.addfinalizer`` one
or multiple times::
- (new in pytest-2.4) by writing a contextmanager fixture # content of conftest.py
generator where a fixture value is "yielded" and the remainder
of the function serves as the teardown code. This integrates
very well with existing context managers.
- by making a fixture function accept a ``request`` argument
with which it can call ``request.addfinalizer(teardownfunction)``
to register a teardown callback function.
Both methods are strictly equivalent from pytest's view and will
remain supported in the future.
Because a number of people prefer the new contextmanager style
we describe it first::
# content of test_ctxfixture.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp():
smtp = smtplib.SMTP("merlinux.eu")
yield smtp # provide the fixture value
print ("teardown smtp")
smtp.close()
pytest detects that you are using a ``yield`` in your fixture function,
turns it into a generator and:
a) iterates once into it for producing the value
b) iterates a second time for tearing the fixture down, expecting
a StopIteration (which is produced automatically from the Python
runtime when the generator returns).
.. note::
The teardown will execute independently of the status of test functions.
You do not need to write the teardown code into a ``try-finally`` clause
like you would usually do with ``contextlib.contextmanager`` decorated
functions.
If the fixture generator yields a second value pytest will report
an error. Yielding cannot be used for parametrization. We'll describe
ways to implement parametrization further below.
Prior to pytest-2.4 you always needed to register a finalizer by accepting
a ``request`` object into your fixture function and calling
``request.addfinalizer`` with a teardown function::
import smtplib import smtplib
import pytest import pytest
@ -299,24 +256,38 @@ a ``request`` object into your fixture function and calling
def fin(): def fin():
print ("teardown smtp") print ("teardown smtp")
smtp.close() smtp.close()
request.addfinalizer(fin)
return smtp # provide the fixture value return smtp # provide the fixture value
This method of registering a finalizer reads more indirect The ``fin`` function will execute when the last test using
than the new contextmanager style syntax because ``fin`` the fixture in the module has finished execution.
is a callback function.
Let's execute it::
$ py.test -s -q --tb=no
FF
2 failed in 0.16 seconds
teardown smtp
We see that the ``smtp`` instance is finalized after the two
tests finished execution. Note that if we decorated our fixture
function with ``scope='function'`` then fixture setup and cleanup would
occur around each single test. In either case the test
module itself does not need to change or know about these details
of fixture setup.
Note that pytest-2.4 introduced an alternative `yield-context <yieldctx>`_
mechanism which allows to interact nicely with context managers.
.. _`request-context`: .. _`request-context`:
Fixtures can interact with the requesting test context Fixtures can introspect the requesting test context
------------------------------------------------------------- -------------------------------------------------------------
pytest provides a builtin :py:class:`request <FixtureRequest>` object, Fixture function can accept the :py:class:`request <FixtureRequest>` object
which fixture functions can use to introspect the function, class or module to introspect the "requesting" test function, class or module context.
for which they are invoked.
Further extending the previous ``smtp`` fixture example, let's Further extending the previous ``smtp`` fixture example, let's
read an optional server URL from the module namespace:: read an optional server URL from the test module which uses our fixture::
# content of conftest.py # content of conftest.py
import pytest import pytest
@ -326,22 +297,21 @@ read an optional server URL from the module namespace::
def smtp(request): def smtp(request):
server = getattr(request.module, "smtpserver", "merlinux.eu") server = getattr(request.module, "smtpserver", "merlinux.eu")
smtp = smtplib.SMTP(server) smtp = smtplib.SMTP(server)
yield smtp # provide the fixture
print ("finalizing %s" % smtp) def fin():
smtp.close() print ("finalizing %s (%s)" % (smtp, server))
smtp.close()
return smtp
The finalizing part after the ``yield smtp`` statement will execute We use the ``request.module`` attribute to optionally obtain an
when the last test using the ``smtp`` fixture has executed:: ``smtpserver`` attribute from the test module. If we just execute
again, nothing much has changed::
$ py.test -s -q --tb=no $ py.test -s -q --tb=no
FF FF
finalizing <smtplib.SMTP instance at 0x1e10248> 2 failed in 0.17 seconds
teardown smtp
We see that the ``smtp`` instance is finalized after the two
tests which use it finished executin. If we rather specify
``scope='function'`` then fixture setup and cleanup occurs
around each single test. Note that in either case the test
module itself does not need to change!
Let's quickly create another test module that actually sets the Let's quickly create another test module that actually sets the
server URL in its module namespace:: server URL in its module namespace::
@ -361,12 +331,11 @@ Running it::
______________________________ test_showhelo _______________________________ ______________________________ test_showhelo _______________________________
test_anothersmtp.py:5: in test_showhelo test_anothersmtp.py:5: in test_showhelo
> assert 0, smtp.helo() > assert 0, smtp.helo()
E AssertionError: (250, 'mail.python.org') E AssertionError: (250, 'hq.merlinux.eu')
voila! The ``smtp`` fixture function picked up our mail server name voila! The ``smtp`` fixture function picked up our mail server name
from the module namespace. from the module namespace.
.. _`fixture-parametrize`: .. _`fixture-parametrize`:
Parametrizing a fixture Parametrizing a fixture
@ -392,9 +361,11 @@ through the special :py:class:`request <FixtureRequest>` object::
params=["merlinux.eu", "mail.python.org"]) params=["merlinux.eu", "mail.python.org"])
def smtp(request): def smtp(request):
smtp = smtplib.SMTP(request.param) smtp = smtplib.SMTP(request.param)
yield smtp def fin():
print ("finalizing %s" % smtp) print ("finalizing %s" % smtp)
smtp.close() smtp.close()
request.addfinalizer(fin)
return smtp
The main change is the declaration of ``params`` with The main change is the declaration of ``params`` with
:py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values :py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values
@ -407,7 +378,7 @@ So let's just do another run::
================================= FAILURES ================================= ================================= FAILURES =================================
__________________________ test_ehlo[merlinux.eu] __________________________ __________________________ test_ehlo[merlinux.eu] __________________________
smtp = <smtplib.SMTP instance at 0x1b38a28> smtp = <smtplib.SMTP instance at 0x1d1b680>
def test_ehlo(smtp): def test_ehlo(smtp):
response = smtp.ehlo() response = smtp.ehlo()
@ -419,7 +390,7 @@ So let's just do another run::
test_module.py:6: AssertionError test_module.py:6: AssertionError
__________________________ test_noop[merlinux.eu] __________________________ __________________________ test_noop[merlinux.eu] __________________________
smtp = <smtplib.SMTP instance at 0x1b38a28> smtp = <smtplib.SMTP instance at 0x1d1b680>
def test_noop(smtp): def test_noop(smtp):
response = smtp.noop() response = smtp.noop()
@ -430,7 +401,7 @@ So let's just do another run::
test_module.py:11: AssertionError test_module.py:11: AssertionError
________________________ test_ehlo[mail.python.org] ________________________ ________________________ test_ehlo[mail.python.org] ________________________
smtp = <smtplib.SMTP instance at 0x1b496c8> smtp = <smtplib.SMTP instance at 0x1d237e8>
def test_ehlo(smtp): def test_ehlo(smtp):
response = smtp.ehlo() response = smtp.ehlo()
@ -441,7 +412,7 @@ So let's just do another run::
test_module.py:5: AssertionError test_module.py:5: AssertionError
________________________ test_noop[mail.python.org] ________________________ ________________________ test_noop[mail.python.org] ________________________
smtp = <smtplib.SMTP instance at 0x1b496c8> smtp = <smtplib.SMTP instance at 0x1d237e8>
def test_noop(smtp): def test_noop(smtp):
response = smtp.noop() response = smtp.noop()
@ -450,6 +421,7 @@ So let's just do another run::
E assert 0 E assert 0
test_module.py:11: AssertionError test_module.py:11: AssertionError
4 failed in 6.04 seconds
We see that our two test functions each ran twice, against the different We see that our two test functions each ran twice, against the different
``smtp`` instances. Note also, that with the ``mail.python.org`` ``smtp`` instances. Note also, that with the ``mail.python.org``
@ -489,13 +461,15 @@ Here we declare an ``app`` fixture which receives the previously defined
$ py.test -v test_appsetup.py $ py.test -v test_appsetup.py
=========================== test session starts ============================ =========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev12 -- /home/hpk/venv/0/bin/python
cachedir: /tmp/doc-exec-120/.cache
plugins: xdist, pep8, cov, cache, capturelog, instafail
collecting ... collected 2 items collecting ... collected 2 items
test_appsetup.py:12: test_smtp_exists[merlinux.eu] PASSED
test_appsetup.py:12: test_smtp_exists[mail.python.org] PASSED test_appsetup.py:12: test_smtp_exists[mail.python.org] PASSED
test_appsetup.py:12: test_smtp_exists[merlinux.eu] PASSED
========================= 2 passed in 5.38 seconds ========================= ========================= 2 passed in 6.98 seconds =========================
Due to the parametrization of ``smtp`` the test will run twice with two Due to the parametrization of ``smtp`` the test will run twice with two
different ``App`` instances and respective smtp servers. There is no different ``App`` instances and respective smtp servers. There is no
@ -534,8 +508,9 @@ to show the setup/teardown flow::
def modarg(request): def modarg(request):
param = request.param param = request.param
print "create", param print "create", param
yield param def fin():
print ("fin %s" % param) print ("fin %s" % param)
return param
@pytest.fixture(scope="function", params=[1,2]) @pytest.fixture(scope="function", params=[1,2])
def otherarg(request): def otherarg(request):
@ -552,31 +527,31 @@ Let's run the tests in verbose mode and with looking at the print-output::
$ py.test -v -s test_module.py $ py.test -v -s test_module.py
=========================== test session starts ============================ =========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev12 -- /home/hpk/venv/0/bin/python
cachedir: /tmp/doc-exec-120/.cache
plugins: xdist, pep8, cov, cache, capturelog, instafail
collecting ... collected 8 items collecting ... collected 8 items
test_module.py:16: test_0[1] PASSED test_module.py:15: test_0[1] PASSED
test_module.py:16: test_0[2] PASSED test_module.py:15: test_0[2] PASSED
test_module.py:18: test_1[mod1] PASSED test_module.py:17: test_1[mod1] PASSED
test_module.py:20: test_2[1-mod1] PASSED test_module.py:19: test_2[1-mod1] PASSED
test_module.py:20: test_2[2-mod1] PASSED test_module.py:19: test_2[2-mod1] PASSED
test_module.py:18: test_1[mod2] PASSED test_module.py:17: test_1[mod2] PASSED
test_module.py:20: test_2[1-mod2] PASSED test_module.py:19: test_2[1-mod2] PASSED
test_module.py:20: test_2[2-mod2] PASSED test_module.py:19: test_2[2-mod2] PASSED
========================= 8 passed in 0.01 seconds ========================= ========================= 8 passed in 0.02 seconds =========================
test0 1 test0 1
test0 2 test0 2
create mod1 create mod1
test1 mod1 test1 mod1
test2 1 mod1 test2 1 mod1
test2 2 mod1 test2 2 mod1
fin mod1
create mod2 create mod2
test1 mod2 test1 mod2
test2 1 mod2 test2 1 mod2
test2 2 mod2 test2 2 mod2
fin mod2
You can see that the parametrized module-scoped ``modarg`` resource caused You can see that the parametrized module-scoped ``modarg`` resource caused
an ordering of test execution that lead to the fewest possible "active" resources. The finalizer for the ``mod1`` parametrized resource was executed an ordering of test execution that lead to the fewest possible "active" resources. The finalizer for the ``mod1`` parametrized resource was executed
@ -632,6 +607,7 @@ to verify our fixture is activated and the tests pass::
$ py.test -q $ py.test -q
.. ..
2 passed in 0.02 seconds
You can specify multiple fixtures like this:: You can specify multiple fixtures like this::
@ -702,6 +678,7 @@ If we run it, we get two passing tests::
$ py.test -q $ py.test -q
.. ..
2 passed in 0.02 seconds
Here is how autouse fixtures work in other scopes: Here is how autouse fixtures work in other scopes:
@ -750,3 +727,62 @@ to a :ref:`conftest.py <conftest.py>` file or even separately installable
fixtures functions starts at test classes, then test modules, then fixtures functions starts at test classes, then test modules, then
``conftest.py`` files and finally builtin and third party plugins. ``conftest.py`` files and finally builtin and third party plugins.
.. _yieldctx:
Fixture functions using "yield" / context manager integration
---------------------------------------------------------------
.. versionadded:: 2.4
pytest-2.4 allows fixture functions to use a ``yield`` instead
of a ``return`` statement to provide a fixture value. Let's
look at a quick example before discussing advantages::
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module", yieldctx=True)
def smtp():
smtp = smtplib.SMTP("merlinux.eu")
yield smtp # provide the fixture value
print ("teardown smtp after a yield")
smtp.close()
In contrast to the `finalization`_ example, our fixture
function uses a single ``yield`` to provide the ``smtp`` fixture
value. The code after the ``yield`` statement serves as the
teardown code, avoiding the indirection of registering a
teardown function. More importantly, it also allows to
seemlessly re-use existing context managers, for example::
@pytest.fixture(yieldctx=True)
def somefixture():
with open("somefile") as f:
yield f.readlines()
The file ``f`` will be closed once ``somefixture`` goes out of scope.
It is possible to achieve the same result by using a ``request.addfinalizer``
call but it is more boilerplate and not very obvious unless
you know about the exact ``__enter__|__exit__`` protocol of with-style
context managers.
For some background, here is the protocol pytest follows for when
``yieldctx=True`` is specified in the fixture decorator:
a) iterate once into the generator for producing the value
b) iterate a second time for tearing the fixture down, expecting
a StopIteration (which is produced automatically from the Python
runtime when the generator returns).
The teardown will always execute, independently of the outcome of
test functions. You do **not need** to write the teardown code into a
``try-finally`` clause like you would usually do with
:py:func:`contextlib.contextmanager` decorated functions.
If the fixture generator yields a second value pytest will report
an error. Yielding cannot be used for parametrization, rather
see `fixture-parametrize`_.

View File

@ -1980,7 +1980,7 @@ class TestContextManagerFixtureFuncs:
def test_simple(self, testdir): def test_simple(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
@pytest.fixture @pytest.fixture(yieldctx=True)
def arg1(): def arg1():
print ("setup") print ("setup")
yield 1 yield 1
@ -2004,7 +2004,7 @@ class TestContextManagerFixtureFuncs:
def test_scoped(self, testdir): def test_scoped(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
@pytest.fixture(scope="module") @pytest.fixture(scope="module", yieldctx=True)
def arg1(): def arg1():
print ("setup") print ("setup")
yield 1 yield 1
@ -2025,7 +2025,7 @@ class TestContextManagerFixtureFuncs:
def test_setup_exception(self, testdir): def test_setup_exception(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
@pytest.fixture(scope="module") @pytest.fixture(scope="module", yieldctx=True)
def arg1(): def arg1():
pytest.fail("setup") pytest.fail("setup")
yield 1 yield 1
@ -2041,7 +2041,7 @@ class TestContextManagerFixtureFuncs:
def test_teardown_exception(self, testdir): def test_teardown_exception(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
@pytest.fixture(scope="module") @pytest.fixture(scope="module", yieldctx=True)
def arg1(): def arg1():
yield 1 yield 1
pytest.fail("teardown") pytest.fail("teardown")
@ -2054,11 +2054,10 @@ class TestContextManagerFixtureFuncs:
*1 passed*1 error* *1 passed*1 error*
""") """)
def test_yields_more_than_one(self, testdir): def test_yields_more_than_one(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
@pytest.fixture(scope="module") @pytest.fixture(scope="module", yieldctx=True)
def arg1(): def arg1():
yield 1 yield 1
yield 2 yield 2
@ -2072,3 +2071,20 @@ class TestContextManagerFixtureFuncs:
""") """)
def test_no_yield(self, testdir):
testdir.makepyfile("""
import pytest
@pytest.fixture(scope="module", yieldctx=True)
def arg1():
return 1
def test_1(arg1):
pass
""")
result = testdir.runpytest("-s")
result.stdout.fnmatch_lines("""
*yieldctx*requires*yield*
*yieldctx=True*
*def arg1*
""")