From b48f1d378b4a9f96425aac848eacf3ef1ee1cce5 Mon Sep 17 00:00:00 2001 From: Hugo Martins Date: Sat, 30 Jun 2018 02:01:49 +0100 Subject: [PATCH] Clarify examples in fixtures' documentation --- changelog/3592.doc.rst | 1 + doc/en/fixture.rst | 155 +++++++++++++++++++++-------------------- 2 files changed, 79 insertions(+), 77 deletions(-) create mode 100644 changelog/3592.doc.rst diff --git a/changelog/3592.doc.rst b/changelog/3592.doc.rst new file mode 100644 index 000000000..cbdf99666 --- /dev/null +++ b/changelog/3592.doc.rst @@ -0,0 +1 @@ +Clarify confusing examples in fixtures' documentation. \ No newline at end of file diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index e07d00eaa..c48caf0af 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -55,18 +55,18 @@ using it:: import pytest @pytest.fixture - def smtp(): + def smtp_connection(): import smtplib return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) - def test_ehlo(smtp): - response, msg = smtp.ehlo() + def test_ehlo(smtp_connection): + response, msg = smtp_connection.ehlo() assert response == 250 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>` -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 =========================== test session starts ============================ @@ -81,8 +81,8 @@ marked ``smtp`` fixture function. Running the test looks like this:: smtp = - def test_ehlo(smtp): - response, msg = smtp.ehlo() + def test_ehlo(smtp_connection): + response, msg = smtp_connection.ehlo() assert response == 250 > assert 0 # for demo purposes E assert 0 @@ -91,16 +91,16 @@ marked ``smtp`` fixture function. Running the test looks like this:: ========================= 1 failed in 0.12 seconds ========================= 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 the exact protocol used by ``pytest`` to call the test function this way: 1. pytest :ref:`finds ` the ``test_ehlo`` because of the ``test_`` prefix. The test function needs a function argument - named ``smtp``. A matching fixture function is discovered by - looking for a fixture-marked function named ``smtp``. + named ``smtp_connection``. A matching fixture function is discovered by + 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()`` is called and fails in the last line of the test function. @@ -167,10 +167,10 @@ Fixtures requiring network access depend on connectivity and are usually time-expensive to create. Extending the previous example, we can add a ``scope="module"`` parameter to the :py:func:`@pytest.fixture <_pytest.python.fixture>` invocation -to cause the decorated ``smtp`` fixture function to only be invoked once -per test *module* (the default is to invoke once per test *function*). +to cause the decorated ``smtp_connection`` fixture function to only be invoked +once per test *module* (the default is to invoke once per test *function*). 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 so that tests from multiple test modules in the directory can @@ -181,23 +181,24 @@ access the fixture function:: import smtplib @pytest.fixture(scope="module") - def smtp(): + def smtp_connection(): return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) -The name of the fixture again is ``smtp`` and you can access its result by -listing the name ``smtp`` as an input parameter in any test or fixture -function (in or below the directory where ``conftest.py`` is located):: +The name of the fixture again is ``smtp_connection`` and you can access its +result by listing the name ``smtp_connection`` as an input parameter in any +test or fixture function (in or below the directory where ``conftest.py`` is +located):: # content of test_module.py - def test_ehlo(smtp): - response, msg = smtp.ehlo() + def test_ehlo(smtp_connection): + response, msg = smtp_connection.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg assert 0 # for demo purposes - def test_noop(smtp): - response, msg = smtp.noop() + def test_noop(smtp_connection): + response, msg = smtp_connection.noop() assert response == 250 assert 0 # for demo purposes @@ -217,8 +218,8 @@ inspect what is going on and can now run the tests:: smtp = - def test_ehlo(smtp): - response, msg = smtp.ehlo() + def test_ehlo(smtp_connection): + response, msg = smtp_connection.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg > assert 0 # for demo purposes @@ -227,10 +228,10 @@ inspect what is going on and can now run the tests:: test_module.py:6: AssertionError ________________________________ test_noop _________________________________ - smtp = + smtp_connection = - def test_noop(smtp): - response, msg = smtp.noop() + def test_noop(smtp_connection): + response, msg = smtp_connection.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 @@ -239,18 +240,18 @@ inspect what is going on and can now run the tests:: ========================= 2 failed in 0.12 seconds ========================= 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 -test functions because pytest shows the incoming argument values in the -traceback. As a result, the two test functions using ``smtp`` run as -quick as a single one because they reuse the same instance. +that the same (module-scoped) ``smtp_connection`` object was passed into the +two test functions because pytest shows the incoming argument values in the +traceback. As a result, the two test functions using ``smtp_connection`` run +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: .. code-block:: python @pytest.fixture(scope="session") - def smtp(): + def smtp_connection(): # the returned fixture value will be shared for # all tests needing it ... @@ -323,9 +324,9 @@ the code after the *yield* statement serves as the teardown code: @pytest.fixture(scope="module") - def smtp(): + def smtp_connection(): smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) - yield smtp # provide the fixture value + yield smtp_connection # provide the fixture value print("teardown smtp") smtp.close() @@ -336,11 +337,11 @@ tests. Let's execute it:: $ pytest -s -q --tb=no - FFteardown smtp + FFteardown smtp_connection 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 function with ``scope='function'`` then fixture setup and cleanup would 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") - def smtp(): + def smtp_connection(): with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp: - yield smtp # provide the fixture value + yield smtp_connection # provide the fixture value -The ``smtp`` connection will be closed after the test finished execution -because the ``smtp`` object automatically closes when +The ``smtp_connection`` connection will be closed after the test finished +execution because the ``smtp_connection`` object automatically closes when the ``with`` statement ends. 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 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 @@ -384,7 +385,7 @@ Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup: @pytest.fixture(scope="module") - def smtp(request): + def smtp_connection(request): smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) def fin(): @@ -425,7 +426,7 @@ Fixtures can introspect the requesting test context Fixture functions can accept the :py:class:`request ` object 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:: # content of conftest.py @@ -433,10 +434,10 @@ read an optional server URL from the test module which uses our fixture:: import smtplib @pytest.fixture(scope="module") - def smtp(request): + def smtp_connection(request): server = getattr(request.module, "smtpserver", "smtp.gmail.com") smtp = smtplib.SMTP(server, 587, timeout=5) - yield smtp + yield smtp_connection print ("finalizing %s (%s)" % (smtp, server)) smtp.close() @@ -456,7 +457,7 @@ server URL in its module namespace:: smtpserver = "mail.python.org" # will be read by smtp fixture - def test_showhelo(smtp): + def test_showhelo(smtp_connection): assert 0, smtp.helo() Running it:: @@ -472,7 +473,7 @@ Running it:: ------------------------- Captured stdout teardown ------------------------- finalizing (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. .. _`fixture-factory`: @@ -541,7 +542,7 @@ write exhaustive functional tests for components which themselves can be configured in multiple ways. 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 through the special :py:class:`request ` object:: @@ -551,9 +552,9 @@ through the special :py:class:`request ` object:: @pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"]) - def smtp(request): + def smtp_connection(request): smtp = smtplib.SMTP(request.param, 587, timeout=5) - yield smtp + yield smtp_connection print ("finalizing %s" % smtp) smtp.close() @@ -568,10 +569,10 @@ So let's just do another run:: ================================= FAILURES ================================= ________________________ test_ehlo[smtp.gmail.com] _________________________ - smtp = + smtp_connection = - def test_ehlo(smtp): - response, msg = smtp.ehlo() + def test_ehlo(smtp_connection): + response, msg = smtp_connection.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg > assert 0 # for demo purposes @@ -580,10 +581,10 @@ So let's just do another run:: test_module.py:6: AssertionError ________________________ test_noop[smtp.gmail.com] _________________________ - smtp = + smtp_connection = - def test_noop(smtp): - response, msg = smtp.noop() + def test_noop(smtp_connection): + response, msg = smtp_connection.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 @@ -591,10 +592,10 @@ So let's just do another run:: test_module.py:11: AssertionError ________________________ test_ehlo[mail.python.org] ________________________ - smtp = + ssmtp_connectionmtp = - def test_ehlo(smtp): - response, msg = smtp.ehlo() + def test_ehlo(smtp_connection): + response, msg = smtp_connection.ehlo() assert response == 250 > 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' @@ -604,10 +605,10 @@ So let's just do another run:: finalizing ________________________ test_noop[mail.python.org] ________________________ - smtp = + smtp_connection = - def test_noop(smtp): - response, msg = smtp.noop() + def test_noop(smtp_connection): + response, msg = smtp_connection.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 @@ -618,7 +619,7 @@ So let's just do another run:: 4 failed in 0.12 seconds 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 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 many projects. As a simple example, we can extend the previous example 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 import pytest class App(object): - def __init__(self, smtp): - self.smtp = smtp + def __init__(self, smtp_connection): + self.smtp_connection = smtp_connection @pytest.fixture(scope="module") - def app(smtp): - return App(smtp) + def app(smtp_connection): + return App(smtp_connection) - def test_smtp_exists(app): - assert app.smtp + def test_smtp_connection_exists(app): + assert app.smtp_connection 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 =========================== test session starts ============================ @@ -762,14 +763,14 @@ Here we declare an ``app`` fixture which receives the previously defined ========================= 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 -need for the ``app`` fixture to be aware of the ``smtp`` parametrization -as pytest will fully analyse the fixture dependency graph. +need for the ``app`` fixture to be aware of the ``smtp_connection`` +parametrization as pytest will fully analyse the fixture dependency graph. Note, that the ``app`` fixture has a scope of ``module`` and uses a -module-scoped ``smtp`` fixture. The example would still work if ``smtp`` -was cached on a ``session`` scope: it is fine for fixtures to use +module-scoped ``smtp_connection`` fixture. The example would still work if +``smtp_connection`` was cached on a ``session`` scope: it is fine for fixtures to use "broader" scoped fixtures but not the other way round: A session-scoped fixture could not use a module-scoped one in a meaningful way.