Testing with pytest Fixtures#
Fixtures are pytest’s way of setting up the data and objects that tests need. Think of them as reusable “preparation steps” that run before your tests.
Instead of copy-pasting setup code into every test function, you define a fixture once and let pytest inject it automatically in tests:
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "role": "admin"}
def test_user_has_name(sample_user):
assert sample_user["name"] == "Alice"
Fixtures can also handle teardown (cleanup after a test), be scoped to run once per module or session, and be parametrized to run the same test against multiple inputs. They aim at keeping the test suite easy to maintain.
Usefull references#
A Complete Guide to Pytest Fixtures — Better Stack (includes parametrizing fixtures)
The conftest.py File#
Pytest has a special file called conftest.py that acts as a shared fixture library. Any fixture defined there is automatically available to every test in the same directory and all its subdirectories.
How it works#
climada/
├── test/
| ├── conftest.py ← fixtures here are available to all tests in climada/test/
| ├── test_engine.py
| └── ...
├── entity/exposures/test/
|── conftest.py ← fixtures here are available only within climada/entity/exposures/test/
└── test_exposures.py
...
Define your fixture in the corresponding conftest.py or in your test file directly depending on its specificity:
import pytest
from climada.entity.exposures
@pytest.fixture
def empty_exposures():
return Exposure()
Then just use it by name in any test that requires it.
def test_empty_exposures(empty_exposures): # pytest injects the fixture automatically
assert empty_exposures.gdf.empty()
Never import fixtures directly. pytest’s injection mechanism won’t work properly with imported fixtures, and you may get confusing errors. Just use the fixture name as a function argument and let pytest handle the rest.
When to use conftest.py#
Put fixtures in |
Keep fixtures in the test file when… |
|---|---|
Multiple test files need them |
Only one test file uses them |
They set up shared resources |
They are very specific to one test scenario |
How to use climada/test/conftest.py for integration tests.#
Our integration test conftest.py defines a set of ready-made CLIMADA objects (exposures, hazard, impact functions) with values chosen so that expected results are rather easy to compute by hand and minimalistic. All fixtures are session-scoped, meaning they are created once and shared across the entire test session.
Here is what is available at a glance:
Fixture |
What it gives you |
|---|---|
|
An |
|
A |
|
An |
|
An |
|
Hazard centroids (slightly offset from exposure points ( |
Using fixtures directly for simple assertions#
When you just need a standard object to test against, request the fixture by name:
def test_impact_aai(exposures, hazard, impfset):
impact = ImpactCalc(exposures, impfset, hazard).impact()
assert impact.aai_agg == pytest.approx(18) # analytically known value
Values of the defaults fixtures were chosen such that impacts are rather easy to compute by hand.
This is documented at the top of conftest.py, but here are some key design choices:
There are 4 events, with frequencies == 0.03, 0.01, 0.006, 0.004, 0, such that impacts for RP250, 100 and 50 and 20 correspond to
at_event, (sorted frequencies cumulate to 1/250, 1/100, 1/50 and 1/20).Hazard intensity is:
Event 1: zero everywhere (always no impact)
Event 2: max intensity (100) at first centroid (also always no impact (first centroid is 0))
Event 3: half max intensity at second centroid (impact == half second centroid)
Event 4: quarter max intensity everywhere (impact == 1/4 total value)
Event 5: max intensity everywhere (but zero frequency)
This results in the following expected values:
Metric |
Expected value |
Why |
|---|---|---|
AAI |
|
Events 3 & 4 weighted by frequency |
RP 20 & 50 |
|
Events 1 & 2 produce zero impact |
RP 100 |
|
Event 3: half intensity on second point (value 1000) |
RP 250 |
|
Event 4: quarter intensity on all points |
Note: The overview above reflects the
conftest.pyfile at the time of writing. If you notice any discrepancy, the docstring at the top ofconftest.pyis the authoritative source.
Using factories for custom scenarios#
When your test needs a variation (e.g. scaled intensity, a different hazard type, group IDs), you can make use of the _factory fixtures. Each factory is a callable that accepts keyword arguments:
def test_scaled_hazard(hazard_factory):
stronger_hazard = hazard_factory(intensity_scale=1.5)
# intensities are halved so expected impacts scale accordingly
def test_grouped_exposure(exposures_factory):
exp = exposures_factory(group_id=np.array([1, 1, 2, 1, 1, 3]))
# exposure with group_id column populated
def test_custom_impf(impf_factory):
impf = impf_factory(paa_scale=0.5)
# PAA halved
Tip: Prefer the direct fixtures (
exposures,hazard, …) when the default setup is sufficient. Reach for factories only when your test specifically targets behaviour that depends on a variation — this keeps tests focused and their intent clear.