Skip to content

Commit 4a62102

Browse files
Merge pull request #2511 from nicoddemus/addfinalizer-docs
fixture docs: highlight difference between yield and addfinalizer methods
2 parents 8c3c430 + f2ba8d7 commit 4a62102

File tree

1 file changed

+89
-71
lines changed

1 file changed

+89
-71
lines changed

doc/en/fixture.rst

Lines changed: 89 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,20 @@ marked ``smtp`` fixture function. Running the test looks like this::
7373
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
7474
rootdir: $REGENDOC_TMPDIR, inifile:
7575
collected 1 items
76-
76+
7777
test_smtpsimple.py F
78-
78+
7979
======= FAILURES ========
8080
_______ test_ehlo ________
81-
81+
8282
smtp = <smtplib.SMTP object at 0xdeadbeef>
83-
83+
8484
def test_ehlo(smtp):
8585
response, msg = smtp.ehlo()
8686
assert response == 250
8787
> assert 0 # for demo purposes
8888
E assert 0
89-
89+
9090
test_smtpsimple.py:11: AssertionError
9191
======= 1 failed in 0.12 seconds ========
9292

@@ -123,20 +123,13 @@ with a list of available function arguments.
123123
but is not anymore advertised as the primary means of declaring fixture
124124
functions.
125125

126-
"Funcargs" a prime example of dependency injection
126+
Fixtures: a prime example of dependency injection
127127
---------------------------------------------------
128128

129-
When injecting fixtures to test functions, pytest-2.0 introduced the
130-
term "funcargs" or "funcarg mechanism" which continues to be present
131-
also in docs today. It now refers to the specific case of injecting
132-
fixture values as arguments to test functions. With pytest-2.3 there are
133-
more possibilities to use fixtures but "funcargs" remain as the main way
134-
as they allow to directly state the dependencies of a test function.
135-
136-
As the following examples show in more detail, funcargs allow test
137-
functions to easily receive and work against specific pre-initialized
138-
application objects without having to care about import/setup/cleanup
139-
details. It's a prime example of `dependency injection`_ where fixture
129+
Fixtures allow test functions to easily receive and work
130+
against specific pre-initialized application objects without having
131+
to care about import/setup/cleanup details.
132+
It's a prime example of `dependency injection`_ where fixture
140133
functions take the role of the *injector* and test functions are the
141134
*consumers* of fixture objects.
142135

@@ -176,7 +169,7 @@ function (in or below the directory where ``conftest.py`` is located)::
176169
response, msg = smtp.ehlo()
177170
assert response == 250
178171
assert b"smtp.gmail.com" in msg
179-
assert 0 # for demo purposes
172+
assert 0 # for demo purposes
180173

181174
def test_noop(smtp):
182175
response, msg = smtp.noop()
@@ -191,32 +184,32 @@ inspect what is going on and can now run the tests::
191184
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
192185
rootdir: $REGENDOC_TMPDIR, inifile:
193186
collected 2 items
194-
187+
195188
test_module.py FF
196-
189+
197190
======= FAILURES ========
198191
_______ test_ehlo ________
199-
192+
200193
smtp = <smtplib.SMTP object at 0xdeadbeef>
201-
194+
202195
def test_ehlo(smtp):
203196
response, msg = smtp.ehlo()
204197
assert response == 250
205198
assert b"smtp.gmail.com" in msg
206199
> assert 0 # for demo purposes
207200
E assert 0
208-
201+
209202
test_module.py:6: AssertionError
210203
_______ test_noop ________
211-
204+
212205
smtp = <smtplib.SMTP object at 0xdeadbeef>
213-
206+
214207
def test_noop(smtp):
215208
response, msg = smtp.noop()
216209
assert response == 250
217210
> assert 0 # for demo purposes
218211
E assert 0
219-
212+
220213
test_module.py:11: AssertionError
221214
======= 2 failed in 0.12 seconds ========
222215

@@ -267,7 +260,7 @@ Let's execute it::
267260

268261
$ pytest -s -q --tb=no
269262
FFteardown smtp
270-
263+
271264
2 failed in 0.12 seconds
272265

273266
We see that the ``smtp`` instance is finalized after the two
@@ -296,36 +289,61 @@ The ``smtp`` connection will be closed after the test finished execution
296289
because the ``smtp`` object automatically closes when
297290
the ``with`` statement ends.
298291

292+
Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the
293+
*teardown* code (after the ``yield``) will not be called.
294+
299295

300296
.. note::
301297
Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one
302298
had to mark a fixture using the ``yield_fixture`` marker. From 2.10 onward, normal
303299
fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed
304300
and considered deprecated.
305301

306-
.. note::
307-
As historical note, another way to write teardown code is
308-
by accepting a ``request`` object into your fixture function and can call its
309-
``request.addfinalizer`` one or multiple times::
310302

311-
# content of conftest.py
303+
An alternative option for executing *teardown* code is to
304+
make use of the ``addfinalizer`` method of the `request-context`_ object to register
305+
finalization functions.
312306

313-
import smtplib
314-
import pytest
307+
Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup:
308+
309+
.. code-block:: python
310+
311+
# content of conftest.py
312+
import smtplib
313+
import pytest
314+
315+
@pytest.fixture(scope="module")
316+
def smtp(request):
317+
smtp = smtplib.SMTP("smtp.gmail.com")
318+
def fin():
319+
print ("teardown smtp")
320+
smtp.close()
321+
request.addfinalizer(fin)
322+
return smtp # provide the fixture value
315323
316-
@pytest.fixture(scope="module")
317-
def smtp(request):
318-
smtp = smtplib.SMTP("smtp.gmail.com")
319-
def fin():
320-
print ("teardown smtp")
321-
smtp.close()
322-
request.addfinalizer(fin)
323-
return smtp # provide the fixture value
324324
325-
The ``fin`` function will execute when the last test in the module has finished execution.
325+
Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test
326+
ends, but ``addfinalizer`` has two key differences over ``yield``:
327+
328+
1. It is possible to register multiple finalizer functions.
329+
330+
2. Finalizers will always be called regardless if the fixture *setup* code raises an exception.
331+
This is handy to properly close all resources created by a fixture even if one of them
332+
fails to be created/acquired::
333+
334+
@pytest.fixture
335+
def equipments(request):
336+
r = []
337+
for port in ('C1', 'C3', 'C28'):
338+
equip = connect(port)
339+
request.addfinalizer(equip.disconnect)
340+
r.append(equip)
341+
return r
342+
343+
In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still
344+
be properly closed. Of course, if an exception happens before the finalize function is
345+
registered then it will not be executed.
326346

327-
This method is still fully supported, but ``yield`` is recommended from 2.10 onward because
328-
it is considered simpler and better describes the natural code flow.
329347

330348
.. _`request-context`:
331349

@@ -355,7 +373,7 @@ again, nothing much has changed::
355373

356374
$ pytest -s -q --tb=no
357375
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
358-
376+
359377
2 failed in 0.12 seconds
360378

361379
Let's quickly create another test module that actually sets the
@@ -423,51 +441,51 @@ So let's just do another run::
423441
FFFF
424442
======= FAILURES ========
425443
_______ test_ehlo[smtp.gmail.com] ________
426-
444+
427445
smtp = <smtplib.SMTP object at 0xdeadbeef>
428-
446+
429447
def test_ehlo(smtp):
430448
response, msg = smtp.ehlo()
431449
assert response == 250
432450
assert b"smtp.gmail.com" in msg
433451
> assert 0 # for demo purposes
434452
E assert 0
435-
453+
436454
test_module.py:6: AssertionError
437455
_______ test_noop[smtp.gmail.com] ________
438-
456+
439457
smtp = <smtplib.SMTP object at 0xdeadbeef>
440-
458+
441459
def test_noop(smtp):
442460
response, msg = smtp.noop()
443461
assert response == 250
444462
> assert 0 # for demo purposes
445463
E assert 0
446-
464+
447465
test_module.py:11: AssertionError
448466
_______ test_ehlo[mail.python.org] ________
449-
467+
450468
smtp = <smtplib.SMTP object at 0xdeadbeef>
451-
469+
452470
def test_ehlo(smtp):
453471
response, msg = smtp.ehlo()
454472
assert response == 250
455473
> assert b"smtp.gmail.com" in msg
456474
E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nSIZE 51200000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8'
457-
475+
458476
test_module.py:5: AssertionError
459477
-------------------------- Captured stdout setup ---------------------------
460478
finalizing <smtplib.SMTP object at 0xdeadbeef>
461479
_______ test_noop[mail.python.org] ________
462-
480+
463481
smtp = <smtplib.SMTP object at 0xdeadbeef>
464-
482+
465483
def test_noop(smtp):
466484
response, msg = smtp.noop()
467485
assert response == 250
468486
> assert 0 # for demo purposes
469487
E assert 0
470-
488+
471489
test_module.py:11: AssertionError
472490
------------------------- Captured stdout teardown -------------------------
473491
finalizing <smtplib.SMTP object at 0xdeadbeef>
@@ -539,7 +557,7 @@ Running the above tests results in the following test IDs being used::
539557
<Function 'test_noop[smtp.gmail.com]'>
540558
<Function 'test_ehlo[mail.python.org]'>
541559
<Function 'test_noop[mail.python.org]'>
542-
560+
543561
======= no tests ran in 0.12 seconds ========
544562

545563
.. _`interdependent fixtures`:
@@ -578,10 +596,10 @@ Here we declare an ``app`` fixture which receives the previously defined
578596
cachedir: .cache
579597
rootdir: $REGENDOC_TMPDIR, inifile:
580598
collecting ... collected 2 items
581-
599+
582600
test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED
583601
test_appsetup.py::test_smtp_exists[mail.python.org] PASSED
584-
602+
585603
======= 2 passed in 0.12 seconds ========
586604

587605
Due to the parametrization of ``smtp`` the test will run twice with two
@@ -647,40 +665,40 @@ Let's run the tests in verbose mode and with looking at the print-output::
647665
cachedir: .cache
648666
rootdir: $REGENDOC_TMPDIR, inifile:
649667
collecting ... collected 8 items
650-
668+
651669
test_module.py::test_0[1] SETUP otherarg 1
652670
RUN test0 with otherarg 1
653671
PASSED TEARDOWN otherarg 1
654-
672+
655673
test_module.py::test_0[2] SETUP otherarg 2
656674
RUN test0 with otherarg 2
657675
PASSED TEARDOWN otherarg 2
658-
676+
659677
test_module.py::test_1[mod1] SETUP modarg mod1
660678
RUN test1 with modarg mod1
661679
PASSED
662680
test_module.py::test_2[1-mod1] SETUP otherarg 1
663681
RUN test2 with otherarg 1 and modarg mod1
664682
PASSED TEARDOWN otherarg 1
665-
683+
666684
test_module.py::test_2[2-mod1] SETUP otherarg 2
667685
RUN test2 with otherarg 2 and modarg mod1
668686
PASSED TEARDOWN otherarg 2
669-
687+
670688
test_module.py::test_1[mod2] TEARDOWN modarg mod1
671689
SETUP modarg mod2
672690
RUN test1 with modarg mod2
673691
PASSED
674692
test_module.py::test_2[1-mod2] SETUP otherarg 1
675693
RUN test2 with otherarg 1 and modarg mod2
676694
PASSED TEARDOWN otherarg 1
677-
695+
678696
test_module.py::test_2[2-mod2] SETUP otherarg 2
679697
RUN test2 with otherarg 2 and modarg mod2
680698
PASSED TEARDOWN otherarg 2
681699
TEARDOWN modarg mod2
682-
683-
700+
701+
684702
======= 8 passed in 0.12 seconds ========
685703

686704
You can see that the parametrized module-scoped ``modarg`` resource caused an
@@ -782,8 +800,8 @@ Autouse fixtures (xUnit setup on steroids)
782800
.. regendoc:wipe
783801
784802
Occasionally, you may want to have fixtures get invoked automatically
785-
without a `usefixtures`_ or `funcargs`_ reference. As a practical
786-
example, suppose we have a database fixture which has a
803+
without declaring a function argument explicitly or a `usefixtures`_ decorator.
804+
As a practical example, suppose we have a database fixture which has a
787805
begin/rollback/commit architecture and we want to automatically surround
788806
each test method by a transaction and a rollback. Here is a dummy
789807
self-contained implementation of this idea::

0 commit comments

Comments
 (0)