introduce yieldctx=True in the @pytest.fixture decorator. Refactor tests and docs.
This commit is contained in:
parent
2bdd034242
commit
3ab9b48782
|
@ -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
|
||||||
|
|
|
@ -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`_.
|
||||||
|
|
||||||
|
|
|
@ -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*
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue