Merge pull request #3642 from caramelomartins/master

Fixes # 3592 - Clarify Fixtures' Documentation
This commit is contained in:
Bruno Oliveira 2018-07-11 23:31:51 -03:00 committed by GitHub
commit 6c3713226c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 98 additions and 96 deletions

1
changelog/3592.doc.rst Normal file
View File

@ -0,0 +1 @@
Use ``smtp_connection`` instead of ``smtp`` in fixtures documentation to avoid possible confusion.

View File

@ -55,18 +55,18 @@ using it::
import pytest import pytest
@pytest.fixture @pytest.fixture
def smtp(): def smtp_connection():
import smtplib import smtplib
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def test_ehlo(smtp): def test_ehlo(smtp_connection):
response, msg = smtp.ehlo() response, msg = smtp_connection.ehlo()
assert response == 250 assert response == 250
assert 0 # for demo purposes assert 0 # for demo purposes
Here, the ``test_ehlo`` needs the ``smtp`` fixture value. pytest Here, the ``test_ehlo`` needs the ``smtp_connection`` fixture value. pytest
will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>` will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>`
marked ``smtp`` fixture function. Running the test looks like this:: marked ``smtp_connection`` fixture function. Running the test looks like this::
$ pytest test_smtpsimple.py $ pytest test_smtpsimple.py
=========================== test session starts ============================ =========================== test session starts ============================
@ -79,10 +79,10 @@ marked ``smtp`` fixture function. Running the test looks like this::
================================= FAILURES ================================= ================================= FAILURES =================================
________________________________ test_ehlo _________________________________ ________________________________ test_ehlo _________________________________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp_connection):
response, msg = smtp.ehlo() response, msg = smtp_connection.ehlo()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
@ -91,18 +91,18 @@ marked ``smtp`` fixture function. Running the test looks like this::
========================= 1 failed in 0.12 seconds ========================= ========================= 1 failed in 0.12 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_connection`` argument, the ``smtplib.SMTP()`` instance created by the fixture
function. The test function fails on our deliberate ``assert 0``. Here is function. The test function fails on our deliberate ``assert 0``. Here is
the exact protocol used by ``pytest`` to call the test function this way: the exact protocol used by ``pytest`` to call the test function this way:
1. pytest :ref:`finds <test discovery>` the ``test_ehlo`` because 1. pytest :ref:`finds <test discovery>` the ``test_ehlo`` because
of the ``test_`` prefix. The test function needs a function argument of the ``test_`` prefix. The test function needs a function argument
named ``smtp``. A matching fixture function is discovered by named ``smtp_connection``. A matching fixture function is discovered by
looking for a fixture-marked function named ``smtp``. looking for a fixture-marked function named ``smtp_connection``.
2. ``smtp()`` is called to create an instance. 2. ``smtp_connection()`` is called to create an instance.
3. ``test_ehlo(<SMTP instance>)`` is called and fails in the last 3. ``test_ehlo(<smtp_connection instance>)`` is called and fails in the last
line of the test function. line of the test function.
Note that if you misspell a function argument or want Note that if you misspell a function argument or want
@ -167,10 +167,10 @@ Fixtures requiring network access depend on connectivity and are
usually time-expensive to create. Extending the previous example, we usually time-expensive to create. Extending the previous example, we
can add a ``scope="module"`` parameter to the can add a ``scope="module"`` parameter to the
:py:func:`@pytest.fixture <_pytest.python.fixture>` invocation :py:func:`@pytest.fixture <_pytest.python.fixture>` invocation
to cause the decorated ``smtp`` fixture function to only be invoked once to cause the decorated ``smtp_connection`` fixture function to only be invoked
per test *module* (the default is to invoke once per test *function*). once per test *module* (the default is to invoke once per test *function*).
Multiple test functions in a test module will thus Multiple test functions in a test module will thus
each receive the same ``smtp`` fixture instance, thus saving time. each receive the same ``smtp_connection`` fixture instance, thus saving time.
The next example puts the fixture function into a separate ``conftest.py`` file The next example puts the fixture function into a separate ``conftest.py`` file
so that tests from multiple test modules in the directory can so that tests from multiple test modules in the directory can
@ -181,23 +181,24 @@ access the fixture function::
import smtplib import smtplib
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def smtp(): def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
The name of the fixture again is ``smtp`` and you can access its result by The name of the fixture again is ``smtp_connection`` and you can access its
listing the name ``smtp`` as an input parameter in any test or fixture result by listing the name ``smtp_connection`` as an input parameter in any
function (in or below the directory where ``conftest.py`` is located):: test or fixture function (in or below the directory where ``conftest.py`` is
located)::
# content of test_module.py # content of test_module.py
def test_ehlo(smtp): def test_ehlo(smtp_connection):
response, msg = smtp.ehlo() response, msg = smtp_connection.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_connection):
response, msg = smtp.noop() response, msg = smtp_connection.noop()
assert response == 250 assert response == 250
assert 0 # for demo purposes assert 0 # for demo purposes
@ -215,10 +216,10 @@ inspect what is going on and can now run the tests::
================================= FAILURES ================================= ================================= FAILURES =================================
________________________________ test_ehlo _________________________________ ________________________________ test_ehlo _________________________________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp_connection):
response, msg = smtp.ehlo() response, msg = smtp_connection.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
@ -227,10 +228,10 @@ 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 object at 0xdeadbeef> smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp_connection):
response, msg = smtp.noop() response, msg = smtp_connection.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
@ -239,18 +240,18 @@ inspect what is going on and can now run the tests::
========================= 2 failed in 0.12 seconds ========================= ========================= 2 failed in 0.12 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_connection`` object was passed into the
test functions because pytest shows the incoming argument values in the two test functions because pytest shows the incoming argument values in the
traceback. As a result, the two test functions using ``smtp`` run as traceback. As a result, the two test functions using ``smtp_connection`` run
quick as a single one because they reuse the same instance. as quick as a single one because they reuse the same instance.
If you decide that you rather want to have a session-scoped ``smtp`` If you decide that you rather want to have a session-scoped ``smtp_connection``
instance, you can simply declare it: instance, you can simply declare it:
.. code-block:: python .. code-block:: python
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def smtp(): def smtp_connection():
# the returned fixture value will be shared for # the returned fixture value will be shared for
# all tests needing it # all tests needing it
... ...
@ -323,11 +324,11 @@ the code after the *yield* statement serves as the teardown code:
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def smtp(): def smtp_connection():
smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
yield smtp # provide the fixture value yield smtp_connection # provide the fixture value
print("teardown smtp") print("teardown smtp")
smtp.close() smtp_connection.close()
The ``print`` and ``smtp.close()`` statements will execute when the last test in The ``print`` and ``smtp.close()`` statements will execute when the last test in
the module has finished execution, regardless of the exception status of the the module has finished execution, regardless of the exception status of the
@ -340,7 +341,7 @@ Let's execute it::
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_connection`` instance is finalized after the two
tests finished execution. Note that if we decorated our fixture tests finished execution. Note that if we decorated our fixture
function with ``scope='function'`` then fixture setup and cleanup would function with ``scope='function'`` then fixture setup and cleanup would
occur around each single test. In either case the test occur around each single test. In either case the test
@ -358,13 +359,13 @@ Note that we can also seamlessly use the ``yield`` syntax with ``with`` statemen
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def smtp(): def smtp_connection():
with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp: with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
yield smtp # provide the fixture value yield smtp_connection # provide the fixture value
The ``smtp`` connection will be closed after the test finished execution The ``smtp_connection`` connection will be closed after the test finished
because the ``smtp`` object automatically closes when execution because the ``smtp_connection`` 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 Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the
@ -374,7 +375,7 @@ An alternative option for executing *teardown* code is to
make use of the ``addfinalizer`` method of the `request-context`_ object to register make use of the ``addfinalizer`` method of the `request-context`_ object to register
finalization functions. finalization functions.
Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup: Here's the ``smtp_connection`` fixture changed to use ``addfinalizer`` for cleanup:
.. code-block:: python .. code-block:: python
@ -384,15 +385,15 @@ Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup:
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def smtp(request): def smtp_connection(request):
smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def fin(): def fin():
print("teardown smtp") print("teardown smtp_connection")
smtp.close() smtp_connection.close()
request.addfinalizer(fin) request.addfinalizer(fin)
return smtp # provide the fixture value return smtp_connection # provide the fixture value
Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test
@ -425,7 +426,7 @@ Fixtures can introspect the requesting test context
Fixture functions can accept the :py:class:`request <FixtureRequest>` object Fixture functions can accept the :py:class:`request <FixtureRequest>` object
to introspect the "requesting" test function, class or module context. to introspect the "requesting" test function, class or module context.
Further extending the previous ``smtp`` fixture example, let's Further extending the previous ``smtp_connection`` fixture example, let's
read an optional server URL from the test module which uses our fixture:: read an optional server URL from the test module which uses our fixture::
# content of conftest.py # content of conftest.py
@ -433,12 +434,12 @@ read an optional server URL from the test module which uses our fixture::
import smtplib import smtplib
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def smtp(request): def smtp_connection(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com") server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp = smtplib.SMTP(server, 587, timeout=5) smtp_connection = smtplib.SMTP(server, 587, timeout=5)
yield smtp yield smtp_connection
print ("finalizing %s (%s)" % (smtp, server)) print ("finalizing %s (%s)" % (smtp_connection, server))
smtp.close() smtp_connection.close()
We use the ``request.module`` attribute to optionally obtain an We use the ``request.module`` attribute to optionally obtain an
``smtpserver`` attribute from the test module. If we just execute ``smtpserver`` attribute from the test module. If we just execute
@ -456,8 +457,8 @@ server URL in its module namespace::
smtpserver = "mail.python.org" # will be read by smtp fixture smtpserver = "mail.python.org" # will be read by smtp fixture
def test_showhelo(smtp): def test_showhelo(smtp_connection):
assert 0, smtp.helo() assert 0, smtp_connection.helo()
Running it:: Running it::
@ -466,13 +467,13 @@ Running it::
================================= FAILURES ================================= ================================= FAILURES =================================
______________________________ 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_connection.helo()
E AssertionError: (250, b'mail.python.org') E AssertionError: (250, b'mail.python.org')
E assert 0 E assert 0
------------------------- Captured stdout teardown ------------------------- ------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef> (mail.python.org) finalizing <smtplib.SMTP object at 0xdeadbeef> (mail.python.org)
voila! The ``smtp`` fixture function picked up our mail server name voila! The ``smtp_connection`` fixture function picked up our mail server name
from the module namespace. from the module namespace.
.. _`fixture-factory`: .. _`fixture-factory`:
@ -541,7 +542,7 @@ write exhaustive functional tests for components which themselves can be
configured in multiple ways. configured in multiple ways.
Extending the previous example, we can flag the fixture to create two Extending the previous example, we can flag the fixture to create two
``smtp`` fixture instances which will cause all tests using the fixture ``smtp_connection`` fixture instances which will cause all tests using the fixture
to run twice. The fixture function gets access to each parameter to run twice. The fixture function gets access to each parameter
through the special :py:class:`request <FixtureRequest>` object:: through the special :py:class:`request <FixtureRequest>` object::
@ -551,11 +552,11 @@ through the special :py:class:`request <FixtureRequest>` object::
@pytest.fixture(scope="module", @pytest.fixture(scope="module",
params=["smtp.gmail.com", "mail.python.org"]) params=["smtp.gmail.com", "mail.python.org"])
def smtp(request): def smtp_connection(request):
smtp = smtplib.SMTP(request.param, 587, timeout=5) smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp yield smtp_connection
print ("finalizing %s" % smtp) print("finalizing %s" % smtp_connection)
smtp.close() smtp_connection.close()
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
@ -568,10 +569,10 @@ So let's just do another run::
================================= FAILURES ================================= ================================= FAILURES =================================
________________________ test_ehlo[smtp.gmail.com] _________________________ ________________________ test_ehlo[smtp.gmail.com] _________________________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp_connection):
response, msg = smtp.ehlo() response, msg = smtp_connection.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
@ -580,10 +581,10 @@ So let's just do another run::
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_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp_connection):
response, msg = smtp.noop() response, msg = smtp_connection.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
@ -591,10 +592,10 @@ 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 object at 0xdeadbeef> smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp_connection):
response, msg = smtp.ehlo() response, msg = smtp_connection.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\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8' E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8'
@ -604,10 +605,10 @@ So let's just do another run::
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_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp_connection):
response, msg = smtp.noop() response, msg = smtp_connection.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
@ -618,7 +619,7 @@ So let's just do another run::
4 failed in 0.12 seconds 4 failed in 0.12 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_connection`` instances. Note also, that with the ``mail.python.org``
connection the second test fails in ``test_ehlo`` because a connection the second test fails in ``test_ehlo`` because a
different server string is expected than what arrived. different server string is expected than what arrived.
@ -730,25 +731,25 @@ can use other fixtures themselves. This contributes to a modular design
of your fixtures and allows re-use of framework-specific fixtures across of your fixtures and allows re-use of framework-specific fixtures across
many projects. As a simple example, we can extend the previous example many projects. As a simple example, we can extend the previous example
and instantiate an object ``app`` where we stick the already defined and instantiate an object ``app`` where we stick the already defined
``smtp`` resource into it:: ``smtp_connection`` resource into it::
# content of test_appsetup.py # content of test_appsetup.py
import pytest import pytest
class App(object): class App(object):
def __init__(self, smtp): def __init__(self, smtp_connection):
self.smtp = smtp self.smtp_connection = smtp_connection
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def app(smtp): def app(smtp_connection):
return App(smtp) return App(smtp_connection)
def test_smtp_exists(app): def test_smtp_connection_exists(app):
assert app.smtp assert app.smtp_connection
Here we declare an ``app`` fixture which receives the previously defined Here we declare an ``app`` fixture which receives the previously defined
``smtp`` fixture and instantiates an ``App`` object with it. Let's run it:: ``smtp_connection`` fixture and instantiates an ``App`` object with it. Let's run it::
$ pytest -v test_appsetup.py $ pytest -v test_appsetup.py
=========================== test session starts ============================ =========================== test session starts ============================
@ -757,19 +758,19 @@ Here we declare an ``app`` fixture which receives the previously defined
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 [ 50%] test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%]
test_appsetup.py::test_smtp_exists[mail.python.org] PASSED [100%] test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%]
========================= 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_connection`` 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
need for the ``app`` fixture to be aware of the ``smtp`` parametrization need for the ``app`` fixture to be aware of the ``smtp_connection``
as pytest will fully analyse the fixture dependency graph. parametrization as pytest will fully analyse the fixture dependency graph.
Note, that the ``app`` fixture has a scope of ``module`` and uses a Note, that the ``app`` fixture has a scope of ``module`` and uses a
module-scoped ``smtp`` fixture. The example would still work if ``smtp`` module-scoped ``smtp_connection`` fixture. The example would still work if
was cached on a ``session`` scope: it is fine for fixtures to use ``smtp_connection`` was cached on a ``session`` scope: it is fine for fixtures to use
"broader" scoped fixtures but not the other way round: "broader" scoped fixtures but not the other way round:
A session-scoped fixture could not use a module-scoped one in a A session-scoped fixture could not use a module-scoped one in a
meaningful way. meaningful way.