-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
/
Copy pathfixtures.rst
1962 lines (1345 loc) · 60.6 KB
/
fixtures.rst
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
.. _how-to-fixtures:
How to use fixtures
====================
.. seealso:: :ref:`about-fixtures`
.. seealso:: :ref:`Fixtures reference <reference-fixtures>`
"Requesting" fixtures
---------------------
At a basic level, test functions request fixtures they require by declaring
them as arguments.
When pytest goes to run a test, it looks at the parameters in that test
function's signature, and then searches for fixtures that have the same names as
those parameters. Once pytest finds them, it runs those fixtures, captures what
they returned (if anything), and passes those objects into the test function as
arguments.
Quick example
^^^^^^^^^^^^^
.. code-block:: python
import pytest
class Fruit:
def __init__(self, name):
self.name = name
self.cubed = False
def cube(self):
self.cubed = True
class FruitSalad:
def __init__(self, *fruit_bowl):
self.fruit = fruit_bowl
self._cube_fruit()
def _cube_fruit(self):
for fruit in self.fruit:
fruit.cube()
# Arrange
@pytest.fixture
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
In this example, ``test_fruit_salad`` "**requests**" ``fruit_bowl`` (i.e.
``def test_fruit_salad(fruit_bowl):``), and when pytest sees this, it will
execute the ``fruit_bowl`` fixture function and pass the object it returns into
``test_fruit_salad`` as the ``fruit_bowl`` argument.
Here's roughly
what's happening if we were to do it by hand:
.. code-block:: python
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
# Arrange
bowl = fruit_bowl()
test_fruit_salad(fruit_bowl=bowl)
Fixtures can **request** other fixtures
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
One of pytest's greatest strengths is its extremely flexible fixture system. It
allows us to boil down complex requirements for tests into more simple and
organized functions, where we only need to have each one describe the things
they are dependent on. We'll get more into this further down, but for now,
here's a quick example to demonstrate how fixtures can use other fixtures:
.. code-block:: python
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
Notice that this is the same example from above, but very little changed. The
fixtures in pytest **request** fixtures just like tests. All the same
**requesting** rules apply to fixtures that do for tests. Here's how this
example would work if we did it by hand:
.. code-block:: python
def first_entry():
return "a"
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)
Fixtures are reusable
^^^^^^^^^^^^^^^^^^^^^
One of the things that makes pytest's fixture system so powerful, is that it
gives us the ability to define a generic setup step that can be reused over and
over, just like a normal function would be used. Two different tests can request
the same fixture and have pytest give each test their own result from that
fixture.
This is extremely useful for making sure tests aren't affected by each other. We
can use this system to make sure each test gets its own fresh batch of data and
is starting from a clean state so it can provide consistent, repeatable results.
Here's an example of how this can come in handy:
.. code-block:: python
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
Each test here is being given its own copy of that ``list`` object,
which means the ``order`` fixture is getting executed twice (the same
is true for the ``first_entry`` fixture). If we were to do this by hand as
well, it would look something like this:
.. code-block:: python
def first_entry():
return "a"
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)
entry = first_entry()
the_list = order(first_entry=entry)
test_int(order=the_list)
A test/fixture can **request** more than one fixture at a time
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Tests and fixtures aren't limited to **requesting** a single fixture at a time.
They can request as many as they like. Here's another quick example to
demonstrate:
.. code-block:: python
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def second_entry():
return 2
# Arrange
@pytest.fixture
def order(first_entry, second_entry):
return [first_entry, second_entry]
# Arrange
@pytest.fixture
def expected_list():
return ["a", 2, 3.0]
def test_string(order, expected_list):
# Act
order.append(3.0)
# Assert
assert order == expected_list
Fixtures can be **requested** more than once per test (return values are cached)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Fixtures can also be **requested** more than once during the same test, and
pytest won't execute them again for that test. This means we can **request**
fixtures in multiple fixtures that are dependent on them (and even again in the
test itself) without those fixtures being executed more than once.
.. code-block:: python
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order():
return []
# Act
@pytest.fixture
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(append_first, order, first_entry):
# Assert
assert order == [first_entry]
If a **requested** fixture was executed once for every time it was **requested**
during a test, then this test would fail because both ``append_first`` and
``test_string_only`` would see ``order`` as an empty list (i.e. ``[]``), but
since the return value of ``order`` was cached (along with any side effects
executing it may have had) after the first time it was called, both the test and
``append_first`` were referencing the same object, and the test saw the effect
``append_first`` had on that object.
.. _`autouse`:
.. _`autouse fixtures`:
Autouse fixtures (fixtures you don't have to request)
-----------------------------------------------------
Sometimes you may want to have a fixture (or even several) that you know all
your tests will depend on. "Autouse" fixtures are a convenient way to make all
tests automatically **request** them. This can cut out a
lot of redundant **requests**, and can even provide more advanced fixture usage
(more on that further down).
We can make a fixture an autouse fixture by passing in ``autouse=True`` to the
fixture's decorator. Here's a simple example for how they can be used:
.. code-block:: python
# contents of test_append.py
import pytest
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return []
@pytest.fixture(autouse=True)
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(order, first_entry):
assert order == [first_entry]
def test_string_and_int(order, first_entry):
order.append(2)
assert order == [first_entry, 2]
In this example, the ``append_first`` fixture is an autouse fixture. Because it
happens automatically, both tests are affected by it, even though neither test
**requested** it. That doesn't mean they *can't* be **requested** though; just
that it isn't *necessary*.
.. _smtpshared:
Scope: sharing fixtures across classes, modules, packages or session
--------------------------------------------------------------------
.. regendoc:wipe
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.fixture>` invocation
to cause a ``smtp_connection`` fixture function, responsible to create a connection to a preexisting SMTP server, 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_connection`` fixture instance, thus saving time.
Possible values for ``scope`` are: ``function``, ``class``, ``module``, ``package`` or ``session``.
The next example puts the fixture function into a separate ``conftest.py`` file
so that tests from multiple test modules in the directory can
access the fixture function:
.. code-block:: python
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
.. code-block:: python
# content of test_module.py
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_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
Here, the ``test_ehlo`` needs the ``smtp_connection`` fixture value. pytest
will discover and call the :py:func:`@pytest.fixture <pytest.fixture>`
marked ``smtp_connection`` fixture function. Running the test looks like this:
.. code-block:: pytest
$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items
test_module.py FF [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
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
E assert 0
test_module.py:7: AssertionError
________________________________ test_noop _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:13: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
============================ 2 failed in 0.12s =============================
You see the two ``assert 0`` failing and more importantly you can also see
that the **exactly same** ``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_connection``
instance, you can simply declare it:
.. code-block:: python
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests requesting it
...
Fixture scopes
^^^^^^^^^^^^^^
Fixtures are created when first requested by a test, and are destroyed based on their ``scope``:
* ``function``: the default scope, the fixture is destroyed at the end of the test.
* ``class``: the fixture is destroyed during teardown of the last test in the class.
* ``module``: the fixture is destroyed during teardown of the last test in the module.
* ``package``: the fixture is destroyed during teardown of the last test in the package where the fixture is defined, including sub-packages and sub-directories within it.
* ``session``: the fixture is destroyed at the end of the test session.
.. note::
Pytest only caches one instance of a fixture at a time, which
means that when using a parametrized fixture, pytest may invoke a fixture more than once in
the given scope.
.. _dynamic scope:
Dynamic scope
^^^^^^^^^^^^^
.. versionadded:: 5.2
In some cases, you might want to change the scope of the fixture without changing the code.
To do that, pass a callable to ``scope``. The callable must return a string with a valid scope
and will be executed only once - during the fixture definition. It will be called with two
keyword arguments - ``fixture_name`` as a string and ``config`` with a configuration object.
This can be especially useful when dealing with fixtures that need time for setup, like spawning
a docker container. You can use the command-line argument to control the scope of the spawned
containers for different environments. See the example below.
.. code-block:: python
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers", None):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
.. _`finalization`:
Teardown/Cleanup (AKA Fixture finalization)
-------------------------------------------
When we run our tests, we'll want to make sure they clean up after themselves so
they don't mess with any other tests (and also so that we don't leave behind a
mountain of test data to bloat the system). Fixtures in pytest offer a very
useful teardown system, which allows us to define the specific steps necessary
for each fixture to clean up after itself.
This system can be leveraged in two ways.
.. _`yield fixtures`:
1. ``yield`` fixtures (recommended)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. regendoc: wipe
"Yield" fixtures ``yield`` instead of ``return``. With these
fixtures, we can run some code and pass an object back to the requesting
fixture/test, just like with the other fixtures. The only differences are:
1. ``return`` is swapped out for ``yield``.
2. Any teardown code for that fixture is placed *after* the ``yield``.
Once pytest figures out a linear order for the fixtures, it will run each one up
until it returns or yields, and then move on to the next fixture in the list to
do the same thing.
Once the test is finished, pytest will go back down the list of fixtures, but in
the *reverse order*, taking each one that yielded, and running the code inside
it that was *after* the ``yield`` statement.
As a simple example, consider this basic email module:
.. code-block:: python
# content of emaillib.py
class MailAdminClient:
def create_user(self):
return MailUser()
def delete_user(self, user):
# do some cleanup
pass
class MailUser:
def __init__(self):
self.inbox = []
def send_email(self, email, other):
other.inbox.append(email)
def clear_mailbox(self):
self.inbox.clear()
class Email:
def __init__(self, subject, body):
self.subject = subject
self.body = body
Let's say we want to test sending email from one user to another. We'll have to
first make each user, then send the email from one user to the other, and
finally assert that the other user received that message in their inbox. If we
want to clean up after the test runs, we'll likely have to make sure the other
user's mailbox is emptied before deleting that user, otherwise the system may
complain.
Here's what that might look like:
.. code-block:: python
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin):
user = mail_admin.create_user()
yield user
user.clear_mailbox()
mail_admin.delete_user(user)
def test_email_received(sending_user, receiving_user):
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
assert email in receiving_user.inbox
Because ``receiving_user`` is the last fixture to run during setup, it's the first to run
during teardown.
There is a risk that even having the order right on the teardown side of things
doesn't guarantee a safe cleanup. That's covered in a bit more detail in
:ref:`safe teardowns`.
.. code-block:: pytest
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
Handling errors for yield fixture
"""""""""""""""""""""""""""""""""
If a yield fixture raises an exception before yielding, pytest won't try to run
the teardown code after that yield fixture's ``yield`` statement. But, for every
fixture that has already run successfully for that test, pytest will still
attempt to tear them down as it normally would.
2. Adding finalizers directly
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
While yield fixtures are considered to be the cleaner and more straightforward
option, there is another choice, and that is to add "finalizer" functions
directly to the test's `request-context`_ object. It brings a similar result as
yield fixtures, but requires a bit more verbosity.
In order to use this approach, we have to request the `request-context`_ object
(just like we would request another fixture) in the fixture we need to add
teardown code for, and then pass a callable, containing that teardown code, to
its ``addfinalizer`` method.
We have to be careful though, because pytest will run that finalizer once it's
been added, even if that fixture raises an exception after adding the finalizer.
So to make sure we don't run the finalizer code when we wouldn't need to, we
would only add the finalizer once the fixture would have done something that
we'd need to teardown.
Here's how the previous example would look using the ``addfinalizer`` method:
.. code-block:: python
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin, request):
user = mail_admin.create_user()
def delete_user():
mail_admin.delete_user(user)
request.addfinalizer(delete_user)
return user
@pytest.fixture
def email(sending_user, receiving_user, request):
_email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(_email, receiving_user)
def empty_mailbox():
receiving_user.clear_mailbox()
request.addfinalizer(empty_mailbox)
return _email
def test_email_received(receiving_user, email):
assert email in receiving_user.inbox
It's a bit longer than yield fixtures and a bit more complex, but it
does offer some nuances for when you're in a pinch.
.. code-block:: pytest
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
Note on finalizer order
""""""""""""""""""""""""
Finalizers are executed in a first-in-last-out order.
For yield fixtures, the first teardown code to run is from the right-most fixture, i.e. the last test parameter.
.. code-block:: python
# content of test_finalizers.py
import pytest
def test_bar(fix_w_yield1, fix_w_yield2):
print("test_bar")
@pytest.fixture
def fix_w_yield1():
yield
print("after_yield_1")
@pytest.fixture
def fix_w_yield2():
yield
print("after_yield_2")
.. code-block:: pytest
$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_finalizers.py test_bar
.after_yield_2
after_yield_1
============================ 1 passed in 0.12s =============================
For finalizers, the first fixture to run is last call to `request.addfinalizer`.
.. code-block:: python
# content of test_finalizers.py
from functools import partial
import pytest
@pytest.fixture
def fix_w_finalizers(request):
request.addfinalizer(partial(print, "finalizer_2"))
request.addfinalizer(partial(print, "finalizer_1"))
def test_bar(fix_w_finalizers):
print("test_bar")
.. code-block:: pytest
$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_finalizers.py test_bar
.finalizer_1
finalizer_2
============================ 1 passed in 0.12s =============================
This is so because yield fixtures use `addfinalizer` behind the scenes: when the fixture executes, `addfinalizer` registers a function that resumes the generator, which in turn calls the teardown code.
.. _`safe teardowns`:
Safe teardowns
--------------
The fixture system of pytest is *very* powerful, but it's still being run by a
computer, so it isn't able to figure out how to safely teardown everything we
throw at it. If we aren't careful, an error in the wrong spot might leave stuff
from our tests behind, and that can cause further issues pretty quickly.
For example, consider the following tests (based off of the mail example from
above):
.. code-block:: python
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def setup():
mail_admin = MailAdminClient()
sending_user = mail_admin.create_user()
receiving_user = mail_admin.create_user()
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
yield receiving_user, email
receiving_user.clear_mailbox()
mail_admin.delete_user(sending_user)
mail_admin.delete_user(receiving_user)
def test_email_received(setup):
receiving_user, email = setup
assert email in receiving_user.inbox
This version is a lot more compact, but it's also harder to read, doesn't have a
very descriptive fixture name, and none of the fixtures can be reused easily.
There's also a more serious issue, which is that if any of those steps in the
setup raise an exception, none of the teardown code will run.
One option might be to go with the ``addfinalizer`` method instead of yield
fixtures, but that might get pretty complex and difficult to maintain (and it
wouldn't be compact anymore).
.. code-block:: pytest
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
.. _`safe fixture structure`:
Safe fixture structure
^^^^^^^^^^^^^^^^^^^^^^
The safest and simplest fixture structure requires limiting fixtures to only
making one state-changing action each, and then bundling them together with
their teardown code, as :ref:`the email examples above <yield fixtures>` showed.
The chance that a state-changing operation can fail but still modify state is
negligible, as most of these operations tend to be `transaction
<https://en.wikipedia.org/wiki/Transaction_processing>`_-based (at least at the
level of testing where state could be left behind). So if we make sure that any
successful state-changing action gets torn down by moving it to a separate
fixture function and separating it from other, potentially failing
state-changing actions, then our tests will stand the best chance at leaving
the test environment the way they found it.
For an example, let's say we have a website with a login page, and we have
access to an admin API where we can generate users. For our test, we want to:
1. Create a user through that admin API
2. Launch a browser using Selenium
3. Go to the login page of our site
4. Log in as the user we created
5. Assert that their name is in the header of the landing page
We wouldn't want to leave that user in the system, nor would we want to leave
that browser session running, so we'll want to make sure the fixtures that
create those things clean up after themselves.
Here's what that might look like:
.. note::
For this example, certain fixtures (i.e. ``base_url`` and
``admin_credentials``) are implied to exist elsewhere. So for now, let's
assume they exist, and we're just not looking at them.
.. code-block:: python
from uuid import uuid4
from urllib.parse import urljoin
from selenium.webdriver import Chrome
import pytest
from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User
@pytest.fixture
def admin_client(base_url, admin_credentials):
return AdminApiClient(base_url, **admin_credentials)
@pytest.fixture
def user(admin_client):
_user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
admin_client.create_user(_user)
yield _user
admin_client.delete_user(_user)
@pytest.fixture
def driver():
_driver = Chrome()
yield _driver
_driver.quit()
@pytest.fixture
def login(driver, base_url, user):
driver.get(urljoin(base_url, "/login"))
page = LoginPage(driver)
page.login(user)
@pytest.fixture
def landing_page(driver, login):
return LandingPage(driver)
def test_name_on_landing_page_after_login(landing_page, user):
assert landing_page.header == f"Welcome, {user.name}!"
The way the dependencies are laid out means it's unclear if the ``user``
fixture would execute before the ``driver`` fixture. But that's ok, because
those are atomic operations, and so it doesn't matter which one runs first
because the sequence of events for the test is still `linearizable
<https://en.wikipedia.org/wiki/Linearizability>`_. But what *does* matter is
that, no matter which one runs first, if the one raises an exception while the
other would not have, neither will have left anything behind. If ``driver``
executes before ``user``, and ``user`` raises an exception, the driver will
still quit, and the user was never made. And if ``driver`` was the one to raise
the exception, then the driver would never have been started and the user would
never have been made.
.. note:
While the ``user`` fixture doesn't *actually* need to happen before the
``driver`` fixture, if we made ``driver`` request ``user``, it might save
some time in the event that making the user raises an exception, since it
won't bother trying to start the driver, which is a fairly expensive
operation.
Running multiple ``assert`` statements safely
---------------------------------------------
Sometimes you may want to run multiple asserts after doing all that setup, which
makes sense as, in more complex systems, a single action can kick off multiple
behaviors. pytest has a convenient way of handling this and it combines a bunch
of what we've gone over so far.
All that's needed is stepping up to a larger scope, then having the **act**
step defined as an autouse fixture, and finally, making sure all the fixtures
are targeting that higher level scope.
Let's pull :ref:`an example from above <safe fixture structure>`, and tweak it a
bit. Let's say that in addition to checking for a welcome message in the header,
we also want to check for a sign out button, and a link to the user's profile.
Let's take a look at how we can structure that so we can run multiple asserts