Initial extracted documentation set
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# Testing
|
||||
|
||||
Use fixtures for reusable resource setup, parametrization for behavior matrices, and explicit boundary seams instead of ad hoc mocking.
|
||||
|
||||
## Why
|
||||
|
||||
Good Python tests optimize for three things at once:
|
||||
|
||||
- local readability
|
||||
- cheap variation across inputs and modes
|
||||
- reusable setup and cleanup without hiding intent
|
||||
|
||||
The mature pattern is not just “use pytest.” It is:
|
||||
|
||||
- model resources with fixtures
|
||||
- make fixture lifetime visible with `yield` when cleanup matters
|
||||
- use parametrization when one behavior should hold across several inputs
|
||||
- test through boundary seams like transports instead of patching internals blindly
|
||||
|
||||
## The pattern
|
||||
|
||||
1. Use fixtures for shared setup and resources.
|
||||
2. Use `yield` fixtures when setup and teardown both matter.
|
||||
3. Use parametrization when the assertion shape is the same but inputs vary.
|
||||
4. Prefer explicit seams over invasive mocking.
|
||||
5. Keep the test body focused on behavior, not scaffolding.
|
||||
|
||||
## When to use
|
||||
|
||||
Use fixtures when:
|
||||
|
||||
- multiple tests need the same resource wiring
|
||||
- setup or cleanup would otherwise dominate the body
|
||||
- the setup is a dependency, not the behavior under test
|
||||
|
||||
Use parametrization when:
|
||||
|
||||
- one behavior should hold across several inputs or modes
|
||||
- the data varies but the test story stays the same
|
||||
|
||||
Use transport or injected seams when:
|
||||
|
||||
- the behavior crosses I/O boundaries
|
||||
- you want realistic flow without spinning up the whole world
|
||||
|
||||
## When not to use
|
||||
|
||||
Do not hide essential behavior behind a giant fixture tower.
|
||||
|
||||
Do not parametrize cases that deserve different narratives or different assertions.
|
||||
|
||||
Do not call fixtures directly like helper functions; if you want a helper, write a helper.
|
||||
|
||||
Do not mock deep internals when a cleaner external seam exists.
|
||||
|
||||
## Preferred shapes
|
||||
|
||||
### Yield fixture for lifecycle
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def resource():
|
||||
obj = make_resource()
|
||||
yield obj
|
||||
obj.close()
|
||||
```
|
||||
|
||||
This keeps setup and teardown obvious.
|
||||
|
||||
### Parametrization for behavior matrices
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("mode", ["prepend", "append", "importlib"])
|
||||
def test_import_behavior(mode: str) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
One behavior, several inputs, no duplicated body.
|
||||
|
||||
### Boundary seam instead of monkeypatch soup
|
||||
|
||||
```python
|
||||
transport = httpx.MockTransport(handler)
|
||||
client = httpx.Client(transport=transport)
|
||||
```
|
||||
|
||||
This is usually cleaner than patching internals in three places.
|
||||
|
||||
## Counterexamples
|
||||
|
||||
### Repeated setup in every test
|
||||
|
||||
```python
|
||||
def test_a():
|
||||
client = make_client()
|
||||
tmpdir = make_tmpdir()
|
||||
seed_db()
|
||||
|
||||
|
||||
def test_b():
|
||||
client = make_client()
|
||||
tmpdir = make_tmpdir()
|
||||
seed_db()
|
||||
```
|
||||
|
||||
The scaffolding overwhelms the behavior.
|
||||
|
||||
### Fixture tower opacity
|
||||
|
||||
If understanding the test requires opening six fixtures before reading one assertion, the abstraction has gone too far.
|
||||
|
||||
### Calling fixtures directly
|
||||
|
||||
Pytest explicitly rejects this because fixtures are injected dependencies, not disguised utility functions.
|
||||
|
||||
## Source signals
|
||||
|
||||
### Pytest
|
||||
|
||||
- `src/_pytest/fixtures.py:1378-1440` makes the contract explicit twice: calling a fixture directly is an error, and `yield` fixtures run teardown code after the test outcome.
|
||||
- `testing/test_threadexception.py:84-91` shows a real `yield` fixture with post-test cleanup work after the `yield`.
|
||||
- `testing/acceptance_test.py:158-169` uses `@pytest.mark.parametrize(...)` to check one behavior across multiple import modes without cloning the test body.
|
||||
- `testing/acceptance_test.py:561-574` shows another compact parametrized case where only the example data changes.
|
||||
|
||||
### HTTPX
|
||||
|
||||
- `httpx/_transports/asgi.py:63-83` exposes `ASGITransport` as an in-process integration seam and even documents `raise_app_exceptions=False` for testing 500 responses.
|
||||
- `httpx/_transports/mock.py:15-43` exposes `MockTransport` as a first-class request/response seam for tests.
|
||||
- `httpx/_client.py:639-660` accepts `transport=` directly on `Client`, which is what makes transport substitution a normal testing path instead of a hack.
|
||||
|
||||
## Bottom line
|
||||
|
||||
A good Python test makes the behavior easy to see and the environment cheap to vary.
|
||||
|
||||
Use fixtures for lifetime.
|
||||
Use parametrization for variation.
|
||||
Use explicit seams instead of brittle patching.
|
||||
Reference in New Issue
Block a user