Skip to content

Commit 773478f

Browse files
authored
Apply suggestions from code review
1 parent 04b86d7 commit 773478f

File tree

1 file changed

+25
-169
lines changed

1 file changed

+25
-169
lines changed

doc/en/how-to/types.rst

Lines changed: 25 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,41 @@ Enhancing Type Annotations with Pytest
66
This page assumes the reader is familiar with Python's typing system and its advantages.
77
For more information, refer to `Python's Typing Documentation <https://docs.python.org/3/library/typing.html>`_.
88

9-
Why Type Tests?
9+
Why type tests?
1010
---------------
1111

12-
Typing tests in pytest provide unique advantages distinct from typing production code. Typed tests emphasize robustness in edge cases and diverse datasets.
13-
Type annotations provide an additional layer of validation, reducing the risk of runtime failures.
12+
Typing tests provides significant advantages:
1413

15-
- **Test Clarity:** Clearly defines expected inputs and outputs, improving readability, especially in complex or parameterized tests.
14+
- **Readability:** Clearly defines expected inputs and outputs, improving readability, especially in complex or parameterized tests.
1615

17-
- **Type Safety:** Helps catch mistakes in test data early, reducing runtime errors.
16+
- **Refactoring:** This is the main benefit in typing tests, as it will greatly help with refactoring, letting the type checker point out the necessary changes in both production and tests, without needing to run the full test suite.
1817

19-
- **Refactoring Support:** Serves as in-code documentation, clarifying data expectations and minimizing errors during test suite modifications.
20-
21-
These benefits make typed tests a powerful tool for maintaining clarity, consistency, and safety throughout the testing process.
22-
23-
Typing Test Functions
24-
---------------------
25-
By adding type annotations to test functions, tests are easier to read and understand.
26-
This is particularly helpful when developers need to refactor code or revisit tests after some time.
27-
28-
For example:
18+
For production code, typing also helps catching some bugs that might not be caught by tests at all (regardless of coverage), for example:
2919

3020
.. code-block:: python
3121
32-
import pytest
22+
def get_caption(target: int, items: list[tuple[int, str]]) -> str:
23+
for value, caption in items:
24+
if value == target:
25+
return caption
26+
27+
28+
The type checker will correctly error out that the function might return `None`, however even a full coverage test suite might miss that case:
3329

30+
.. code-block:: python
3431
35-
def add(a: int, b: int) -> int:
36-
return a + b
32+
def test_get_caption() -> None:
33+
assert get_caption(10, [(1, "foo"), (10, "bar")]) == "bar"
3734
3835
39-
def test_add() -> None:
40-
result = add(2, 3)
41-
assert result == 5
36+
Note the code above has 100% coverage, but the bug is not caught (of course the example is "obvious", but serves to illustrate the point).
4237

43-
Here, `test_add` is annotated with `-> None`, as it does not return a value.
44-
While `-> None` typing may seem unnecessary, it ensures type checkers validate the function and helps identifying potential issues during refactoring.
4538

4639

47-
Typing Fixtures
40+
Typing fixtures
4841
---------------
49-
Fixtures in pytest helps set up data or provides resources needed for tests.
50-
Adding type annotations to fixtures makes it clear what data they return, which helps with debugging and readability.
5142

52-
* Basic Fixture Typing
43+
To type fixtures in pytest, just add normal types to the fixture functions -- there is nothing special that needs to be done just because of the `fixture` decorator.
5344

5445
.. code-block:: python
5546
@@ -59,178 +50,43 @@ Adding type annotations to fixtures makes it clear what data they return, which
5950
@pytest.fixture
6051
def sample_fixture() -> int:
6152
return 38
53+
54+
In the same manner, the fixtures passed to test functions need be annotated with the fixture's return type:
6255

56+
.. code-block:: python
6357
6458
def test_sample_fixture(sample_fixture: int) -> None:
6559
assert sample_fixture == 38
6660
67-
Here, `sample_fixture()` is typed to return an `int`. This ensures consistency and helps identify mismatch types during refactoring.
68-
69-
70-
* Typing Fixtures with Lists and Dictionaries
71-
This example shows how to use List and Dict types in pytest.
72-
73-
.. code-block:: python
74-
75-
from typing import List, Dict
76-
import pytest
77-
78-
79-
@pytest.fixture
80-
def sample_list() -> List[int]:
81-
return [5, 10, 15]
82-
61+
From the POV of the type checker, it does not matter that `sample_fixture` is actually a fixture managed by pytest, all it matters to it is that `sample_fixture` is a parameter of type `int`.
8362

84-
def test_sample_list(sample_list: List[int]) -> None:
85-
assert sum(sample_list) == 30
8663

87-
88-
@pytest.fixture
89-
def sample_dict() -> Dict[str, int]:
90-
return {"a": 50, "b": 100}
91-
92-
93-
def test_sample_dict(sample_dict: Dict[str, int]) -> None:
94-
assert sample_dict["a"] == 50
95-
96-
Annotating fixtures with types like List[int] and Dict[str, int] ensures data consistency and helps prevent runtime errors when performing operations.
97-
This ensures that only `int` values are allowed in the list and that `str` keys map to `int` values in the dictionary, helping avoid type-related issues.
98-
99-
Typing Parameterized Tests
100-
--------------------------
101-
With `@pytest.mark.parametrize`, adding typing annotations to the input parameters reinforce type safety and reduce errors with multiple data sets.
102-
103-
For example, you are testing if adding 1 to `input_value` results in `expected_output` for each set of arguments.
64+
The same logic applies to `@pytest.mark.parametrize`:
10465

10566
.. code-block:: python
10667
10768
import pytest
10869
109-
11070
@pytest.mark.parametrize("input_value, expected_output", [(1, 2), (5, 6), (10, 11)])
11171
def test_increment(input_value: int, expected_output: int) -> None:
11272
assert input_value + 1 == expected_output
11373
114-
Here, typing clarifies that both `input_value` and `expected_output` are expected as integers, promoting consistency.
115-
While parameterized tests can involve varied data types and that annotations simplify maintenance when datasets grow.
11674
11775
118-
Typing for Monkeypatching
119-
-------------------------
120-
Monkeypatching modifies functions or environment variables during runtime.
121-
Adding typing, such as `monkeypatch: pytest.MonkeyPatch`, clarifies the expected patching behaviour and reduces the risk of errors.
12276
123-
* Example of Typing Monkeypatching Environment Variables
124-
125-
This example is based on the pytest documentation for `Monkeypatching <https://github.com/pytest-dev/pytest/blob/main/doc/en/how-to/monkeypatch.rst>`_, with the addition of typing annotations.
77+
The same logic applies when typing fixture functions which receive other fixtures:
12678

12779
.. code-block:: python
12880
129-
# contents of our original code file e.g. code.py
130-
import pytest
131-
import os
132-
from typing import Optional
133-
134-
135-
def get_os_user_lower() -> str:
136-
"""Simple retrieval function. Returns lowercase USER or raises OSError."""
137-
username: Optional[str] = os.getenv("USER")
138-
139-
if username is None:
140-
raise OSError("USER environment is not set.")
141-
142-
return username.lower()
143-
144-
145-
# contents of our test file e.g. test_code.py
14681
@pytest.fixture
14782
def mock_env_user(monkeypatch: pytest.MonkeyPatch) -> None:
14883
monkeypatch.setenv("USER", "TestingUser")
14984
15085
151-
@pytest.fixture
152-
def mock_env_missing(monkeypatch: pytest.MonkeyPatch) -> None:
153-
monkeypatch.delenv("USER", raising=False)
154-
155-
156-
def test_upper_to_lower(mock_env_user: None) -> None:
157-
assert get_os_user_lower() == "testinguser"
158-
159-
160-
def test_raise_exception(mock_env_missing: None) -> None:
161-
with pytest.raises(OSError):
162-
_ = get_os_user_lower()
163-
164-
Here:
165-
166-
- **username: Optional[str]:** Indicates the variable `username` may either be a string or `None`.
167-
- **get_os_user_lower() -> str:** Specifies this function will return a string, providing explicit return value type.
168-
- **monkeypatch fixture is typed as pytest.MonkeyPatch:** Shows that it will provide an object for patching environment variables during the test. This clarifies the intended use of the fixture and helps developers to use it correctly.
169-
- **Fixture return -> None, like mock_env_user:** Specifies they do not return any value, but instead modify the test environment.
170-
171-
Typing annotations can also be extended to `monkeypatch` usage in pytest for class methods, instance attributes, or standalone functions.
172-
This enhances type safety and clarity when patching the test environment.
173-
174-
175-
Typing Temporary Directories and Paths
176-
--------------------------------------
177-
Temporary directories and paths are commonly used in pytest to create isolated environments for testing file and directory operations.
178-
The `tmp_path` and `tmpdir` fixtures provide these capabilities.
179-
Adding typing annotations enhances clarity about the types of objects these fixtures return, which is particularly useful when performing file operations.
180-
181-
Below examples are based on the pytest documentation for `Temporary Directories and Files in tests <https://github.com/pytest-dev/pytest/blob/main/doc/en/how-to/tmp_path.rst>`_, with the addition of typing annotations.
182-
183-
* Typing with `tmp_path` for File Creation
184-
185-
.. code-block:: python
186-
187-
import pytest
188-
from pathlib import Path
18986
190-
# content of test_tmp_path.py
191-
CONTENT = "content"
192-
193-
194-
def test_create_file(tmp_path: Path) -> None:
195-
d = tmp_path / "sub"
196-
d.mkdir()
197-
p = d / "hello.txt"
198-
p.write_text(CONTENT, encoding="utf-8")
199-
assert p.read_text(encoding="utf-8") == CONTENT
200-
assert len(list(tmp_path.iterdir())) == 1
201-
202-
Typing `tmp_path: Path` explicitly defines it as a Path object, improving code readability and catching type issues early.
203-
204-
* Typing with `tmp_path_factory` fixture for creating temporary files during a session
205-
206-
.. code-block:: python
207-
208-
# contents of conftest.py
209-
import pytest
210-
from pathlib import Path
211-
212-
213-
@pytest.fixture(scope="session")
214-
def image_file(tmp_path_factory: pytest.TempPathFactory) -> Path:
215-
img = compute_expensive_image()
216-
fn: Path = tmp_path_factory.mktemp("data") / "img.png"
217-
img.save(fn)
218-
return fn
219-
220-
221-
# contents of test_image.py
222-
def test_histogram(image_file: Path) -> None:
223-
img = load_image(image_file)
224-
# compute and test histogram
225-
226-
Here:
227-
228-
- **tmp_path_factory: pytest.TempPathFactory:** Indicates that `tmp_path_factory` is an instance of pytest’s `TempPathFactory`, responsible for creating temporary directories and paths during testing.
229-
- **fn: Path:** Identifies that `fn` is a `Path` object, emphasizing its role as a file path and clarifying the expected file operations.
230-
- **Return type -> Path:** Specifies the fixture returns a `Path` object, clarifying its expected structure.
231-
- **image_file: Path:** Defines `image_file` as a Path object, ensuring compatibility with `load_image`.
23287
23388
Conclusion
23489
----------
90+
23591
Incorporating typing into pytest tests enhances **clarity**, improves **debugging** and **maintenance**, and ensures **type safety**.
23692
These practices lead to a **robust**, **readable**, and **easily maintainable** test suite that is better equipped to handle future changes with minimal risk of errors.

0 commit comments

Comments
 (0)