Merge pull request #2511 from nicoddemus/addfinalizer-docs

fixture docs: highlight difference between yield and addfinalizer methods
This commit is contained in:
Ronny Pfannschmidt 2017-06-22 07:59:44 +02:00 committed by GitHub
commit 4a62102b57
1 changed files with 89 additions and 71 deletions

View File

@ -73,20 +73,20 @@ marked ``smtp`` fixture function. Running the test looks like this::
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collected 1 items collected 1 items
test_smtpsimple.py F test_smtpsimple.py F
======= FAILURES ======== ======= FAILURES ========
_______ test_ehlo ________ _______ test_ehlo ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_smtpsimple.py:11: AssertionError test_smtpsimple.py:11: AssertionError
======= 1 failed in 0.12 seconds ======== ======= 1 failed in 0.12 seconds ========
@ -123,20 +123,13 @@ 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 Fixtures: a prime example of dependency injection
--------------------------------------------------- ---------------------------------------------------
When injecting fixtures to test functions, pytest-2.0 introduced the Fixtures allow test functions to easily receive and work
term "funcargs" or "funcarg mechanism" which continues to be present against specific pre-initialized application objects without having
also in docs today. It now refers to the specific case of injecting to care about import/setup/cleanup details.
fixture values as arguments to test functions. With pytest-2.3 there are It's a prime example of `dependency injection`_ where fixture
more possibilities to use fixtures but "funcargs" remain as the main way
as they allow to directly state the dependencies of a test function.
As the following examples show in more detail, funcargs allow test
functions to easily receive and work against specific pre-initialized
application objects without having to care about import/setup/cleanup
details. It's a prime example of `dependency injection`_ where fixture
functions take the role of the *injector* and test functions are the functions take the role of the *injector* and test functions are the
*consumers* of fixture objects. *consumers* of fixture objects.
@ -176,7 +169,7 @@ function (in or below the directory where ``conftest.py`` is located)::
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
assert b"smtp.gmail.com" in msg assert b"smtp.gmail.com" in msg
assert 0 # for demo purposes assert 0 # for demo purposes
def test_noop(smtp): def test_noop(smtp):
response, msg = smtp.noop() response, msg = smtp.noop()
@ -191,32 +184,32 @@ inspect what is going on and can now run the tests::
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collected 2 items collected 2 items
test_module.py FF test_module.py FF
======= FAILURES ======== ======= FAILURES ========
_______ test_ehlo ________ _______ test_ehlo ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
assert b"smtp.gmail.com" in msg assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:6: AssertionError test_module.py:6: AssertionError
_______ test_noop ________ _______ test_noop ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp):
response, msg = smtp.noop() response, msg = smtp.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:11: AssertionError test_module.py:11: AssertionError
======= 2 failed in 0.12 seconds ======== ======= 2 failed in 0.12 seconds ========
@ -267,7 +260,7 @@ Let's execute it::
$ pytest -s -q --tb=no $ pytest -s -q --tb=no
FFteardown smtp FFteardown smtp
2 failed in 0.12 seconds 2 failed in 0.12 seconds
We see that the ``smtp`` instance is finalized after the two We see that the ``smtp`` instance is finalized after the two
@ -296,6 +289,9 @@ The ``smtp`` connection will be closed after the test finished execution
because the ``smtp`` object automatically closes when because the ``smtp`` object automatically closes when
the ``with`` statement ends. the ``with`` statement ends.
Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the
*teardown* code (after the ``yield``) will not be called.
.. note:: .. note::
Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one
@ -303,29 +299,51 @@ the ``with`` statement ends.
fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed
and considered deprecated. and considered deprecated.
.. note::
As historical note, another way to write teardown code is
by accepting a ``request`` object into your fixture function and can call its
``request.addfinalizer`` one or multiple times::
# content of conftest.py An alternative option for executing *teardown* code is to
make use of the ``addfinalizer`` method of the `request-context`_ object to register
finalization functions.
import smtplib Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup:
import pytest
@pytest.fixture(scope="module") .. code-block:: python
def smtp(request):
smtp = smtplib.SMTP("smtp.gmail.com")
def fin():
print ("teardown smtp")
smtp.close()
request.addfinalizer(fin)
return smtp # provide the fixture value
The ``fin`` function will execute when the last test in the module has finished execution. # content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp(request):
smtp = smtplib.SMTP("smtp.gmail.com")
def fin():
print ("teardown smtp")
smtp.close()
request.addfinalizer(fin)
return smtp # provide the fixture value
Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test
ends, but ``addfinalizer`` has two key differences over ``yield``:
1. It is possible to register multiple finalizer functions.
2. Finalizers will always be called regardless if the fixture *setup* code raises an exception.
This is handy to properly close all resources created by a fixture even if one of them
fails to be created/acquired::
@pytest.fixture
def equipments(request):
r = []
for port in ('C1', 'C3', 'C28'):
equip = connect(port)
request.addfinalizer(equip.disconnect)
r.append(equip)
return r
In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still
be properly closed. Of course, if an exception happens before the finalize function is
registered then it will not be executed.
This method is still fully supported, but ``yield`` is recommended from 2.10 onward because
it is considered simpler and better describes the natural code flow.
.. _`request-context`: .. _`request-context`:
@ -355,7 +373,7 @@ again, nothing much has changed::
$ pytest -s -q --tb=no $ pytest -s -q --tb=no
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com) FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
2 failed in 0.12 seconds 2 failed in 0.12 seconds
Let's quickly create another test module that actually sets the Let's quickly create another test module that actually sets the
@ -423,51 +441,51 @@ So let's just do another run::
FFFF FFFF
======= FAILURES ======== ======= FAILURES ========
_______ test_ehlo[smtp.gmail.com] ________ _______ test_ehlo[smtp.gmail.com] ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
assert b"smtp.gmail.com" in msg assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:6: AssertionError test_module.py:6: AssertionError
_______ test_noop[smtp.gmail.com] ________ _______ test_noop[smtp.gmail.com] ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp):
response, msg = smtp.noop() response, msg = smtp.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:11: AssertionError test_module.py:11: AssertionError
_______ test_ehlo[mail.python.org] ________ _______ test_ehlo[mail.python.org] ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
> assert b"smtp.gmail.com" in msg > assert b"smtp.gmail.com" in msg
E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nSIZE 51200000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8' E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nSIZE 51200000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8'
test_module.py:5: AssertionError test_module.py:5: AssertionError
-------------------------- Captured stdout setup --------------------------- -------------------------- Captured stdout setup ---------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef> finalizing <smtplib.SMTP object at 0xdeadbeef>
_______ test_noop[mail.python.org] ________ _______ test_noop[mail.python.org] ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp):
response, msg = smtp.noop() response, msg = smtp.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:11: AssertionError test_module.py:11: AssertionError
------------------------- Captured stdout teardown ------------------------- ------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef> finalizing <smtplib.SMTP object at 0xdeadbeef>
@ -539,7 +557,7 @@ Running the above tests results in the following test IDs being used::
<Function 'test_noop[smtp.gmail.com]'> <Function 'test_noop[smtp.gmail.com]'>
<Function 'test_ehlo[mail.python.org]'> <Function 'test_ehlo[mail.python.org]'>
<Function 'test_noop[mail.python.org]'> <Function 'test_noop[mail.python.org]'>
======= no tests ran in 0.12 seconds ======== ======= no tests ran in 0.12 seconds ========
.. _`interdependent fixtures`: .. _`interdependent fixtures`:
@ -578,10 +596,10 @@ Here we declare an ``app`` fixture which receives the previously defined
cachedir: .cache cachedir: .cache
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 2 items collecting ... collected 2 items
test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED
test_appsetup.py::test_smtp_exists[mail.python.org] PASSED test_appsetup.py::test_smtp_exists[mail.python.org] PASSED
======= 2 passed in 0.12 seconds ======== ======= 2 passed in 0.12 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
@ -647,26 +665,26 @@ Let's run the tests in verbose mode and with looking at the print-output::
cachedir: .cache cachedir: .cache
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 8 items collecting ... collected 8 items
test_module.py::test_0[1] SETUP otherarg 1 test_module.py::test_0[1] SETUP otherarg 1
RUN test0 with otherarg 1 RUN test0 with otherarg 1
PASSED TEARDOWN otherarg 1 PASSED TEARDOWN otherarg 1
test_module.py::test_0[2] SETUP otherarg 2 test_module.py::test_0[2] SETUP otherarg 2
RUN test0 with otherarg 2 RUN test0 with otherarg 2
PASSED TEARDOWN otherarg 2 PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod1] SETUP modarg mod1 test_module.py::test_1[mod1] SETUP modarg mod1
RUN test1 with modarg mod1 RUN test1 with modarg mod1
PASSED PASSED
test_module.py::test_2[1-mod1] SETUP otherarg 1 test_module.py::test_2[1-mod1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod1 RUN test2 with otherarg 1 and modarg mod1
PASSED TEARDOWN otherarg 1 PASSED TEARDOWN otherarg 1
test_module.py::test_2[2-mod1] SETUP otherarg 2 test_module.py::test_2[2-mod1] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod1 RUN test2 with otherarg 2 and modarg mod1
PASSED TEARDOWN otherarg 2 PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod2] TEARDOWN modarg mod1 test_module.py::test_1[mod2] TEARDOWN modarg mod1
SETUP modarg mod2 SETUP modarg mod2
RUN test1 with modarg mod2 RUN test1 with modarg mod2
@ -674,13 +692,13 @@ Let's run the tests in verbose mode and with looking at the print-output::
test_module.py::test_2[1-mod2] SETUP otherarg 1 test_module.py::test_2[1-mod2] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod2 RUN test2 with otherarg 1 and modarg mod2
PASSED TEARDOWN otherarg 1 PASSED TEARDOWN otherarg 1
test_module.py::test_2[2-mod2] SETUP otherarg 2 test_module.py::test_2[2-mod2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod2 RUN test2 with otherarg 2 and modarg mod2
PASSED TEARDOWN otherarg 2 PASSED TEARDOWN otherarg 2
TEARDOWN modarg mod2 TEARDOWN modarg mod2
======= 8 passed in 0.12 seconds ======== ======= 8 passed in 0.12 seconds ========
You can see that the parametrized module-scoped ``modarg`` resource caused an You can see that the parametrized module-scoped ``modarg`` resource caused an
@ -782,8 +800,8 @@ Autouse fixtures (xUnit setup on steroids)
.. regendoc:wipe .. regendoc:wipe
Occasionally, you may want to have fixtures get invoked automatically Occasionally, you may want to have fixtures get invoked automatically
without a `usefixtures`_ or `funcargs`_ reference. As a practical without declaring a function argument explicitly or a `usefixtures`_ decorator.
example, suppose we have a database fixture which has a As a practical example, suppose we have a database fixture which has a
begin/rollback/commit architecture and we want to automatically surround begin/rollback/commit architecture and we want to automatically surround
each test method by a transaction and a rollback. Here is a dummy each test method by a transaction and a rollback. Here is a dummy
self-contained implementation of this idea:: self-contained implementation of this idea::