Skip to content

Commit c98d1a8

Browse files
jiridanekjstourac
andcommitted
NO-JIRA: docs(tests/): add interactive PyTest tutorial
Co-authored-by: Jan Stourac <[email protected]>
1 parent c7e44ae commit c98d1a8

File tree

1 file changed

+122
-0
lines changed

1 file changed

+122
-0
lines changed
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Welcome!
2+
Welcome to our pytest-based testsuite.
3+
4+
This series of tests is designed to help you understand how pytest works,
5+
and how we use it to test our workbench images."""
6+
7+
8+
def test_hello_world():
9+
"""There are three ways to specify a test in PyTest.
10+
This one is the most straightforward.
11+
12+
In a file that in named test_*.py or *_test.py,
13+
define a function named test_*, and pytest will discover it and run it as a test."""
14+
15+
# Assertions in pytest are usually done with the `assert` keyword.
16+
# Pytest does magicks to produce an "expected x but got y"-style message from this.
17+
# In normal Python code without pytest, we'd only get a generic AssertionError exception without these details.
18+
assert 2 + 2 == 4
19+
20+
# We are ok with having multiple assertions in a single test.
21+
# Subtests are often useful in such situations. That will be covered later.
22+
pod = "placeholder_value"
23+
# The customizable failre message with a f-string is a practical way to report more context
24+
# about a test failure when the assertion is not met.
25+
assert 2 + 2 == 4, f"Expectations about {pod=} were not fulfilled."
26+
27+
28+
"""Now it's a good time to run the test above for yourself.
29+
30+
```
31+
uv run pytest tests/pytest_tutorial/test_01_intro.py -k test_hello_world
32+
```
33+
34+
Try adjusting the numbers in one of the assertions, rerun, and see the test fail.
35+
"""
36+
37+
38+
class TestSomething:
39+
"""Tests can be also defined in a class named Test*"""
40+
41+
# def __init__(self):
42+
# """Pytest test classes may not have a constructor (i.e., the __init__ method).
43+
#
44+
# If you uncomment this method and run the test suite, you'd see something like this:
45+
# tests/pytest_tutorial/test_01_intro.py:38
46+
# /Users/jdanek/IdeaProjects/notebooks/tests/pytest_tutorial/test_01_intro.py:38: PytestCollectionWarning: cannot collect test class 'TestSomething' because it has a __init__ constructor (from: tests/pytest_tutorial/test_01_intro.py)
47+
# class TestSomething:
48+
#
49+
# and the test_something method below will not be run as a test."""
50+
51+
def test_something(self):
52+
"""Individual tests are methods in this class, and they are named test_* as usual."""
53+
assert True is not False
54+
55+
56+
import unittest
57+
58+
59+
class LegacyThing(unittest.TestCase):
60+
"""Last option, tests can be defined using the unittest package in the standard library.
61+
Pytest is compatible with unittest and will run these tests just fine.
62+
63+
The important aspect for test discovery is that the class inherits from unittest.TestCase."""
64+
65+
@classmethod
66+
def setUpClass(cls):
67+
"""This classmethod will be run before any tests."""
68+
69+
def setUp(self):
70+
"""This method will be run before each test method."""
71+
72+
def test_something(self):
73+
"""Test methods still need to adhere to the test_* naming pattern."""
74+
self.assertEqual(2 + 2, 4)
75+
76+
def tearDown(self):
77+
"""This method will be run after each test method"""
78+
79+
@classmethod
80+
def tearDownClass(cls):
81+
"""This method will be run after all test methods were run."""
82+
83+
84+
"""Pytest runs in two phases: test collection and then test execution.
85+
In the collection phase, all files matching the pattern (test_*.py or *_test.py) are imported and scanned
86+
for test functions and methods, and then pytest runs what it discovered.
87+
88+
# Only perform collection (test discovery), do not execute anything:
89+
```
90+
uv run pytest tests/pytest_tutorial/test_01_intro.py --collect-only
91+
```
92+
93+
In the execution phase, the collected tests are executed.
94+
95+
Pytest will initialize and then tear down any fixtures the tests may be using, as needed.
96+
This is the idiomatic way of handling unittest's setUp and tearDown in Pytest.
97+
See one of the follow-up tutorials about fixtures for more.
98+
"""
99+
100+
101+
class TestCapture:
102+
def test_something_that_prints(self):
103+
"""Pytest has a built-in mechanism for capturing stdout and stderr.
104+
This is enabled by default, and it causes all output to be printed at the end of a test run,
105+
and only for tests that have failed.
106+
While it makes sense for unittests, we use stdout to track progress in the long-running tests here.
107+
108+
Therefore, in our `pytest.ini`, we have disabled the capture (and set less verbose exceptions output):
109+
110+
```
111+
[pytest]
112+
addopts = --capture=no --tb=short
113+
```
114+
"""
115+
print("Hello, world")
116+
# Uncomment the three lines below.
117+
# Without `--capture=no` you would never see the Hello, world printed while waiting.
118+
# the default value for `--capture` is `--capture=fd`.
119+
120+
# import time
121+
# time.sleep(10)
122+
# assert False

0 commit comments

Comments
 (0)