Review testing routines
Update 2026-07-08: This issue was originally opened against an older snapshot of
class_definitions.py (108 statements, ~66% coverage). The codebase has moved on
significantly since then. This body has been rewritten to reflect the current state,
answer the open structural questions, and give a concrete implementation plan. The
original coverage table is preserved in the issue's edit history.
Current coverage (as of main @ 4fc636a, measured with pixi run pytest tests/ --cov=pypsa_validation_processing --cov-report=term-missing)
| module |
statements |
coverage |
class_definitions.py |
313 |
87% |
statistics_functions.py |
172 |
98% |
utils.py |
31 |
100% |
workflow.py |
24 |
96% |
| total |
542 |
92% |
The repo is already well past 80% overall. class_definitions.py is the weakest module and
the remaining gaps are narrow — mostly untested error branches (missing-path
FileNotFoundError/ValueError checks in __init__) and one multi-unit-parsing branch,
not a systemic hole. The README's testing-batch command referenced in the original body
no longer exists in README.md — that item appears already resolved.
Critical finding: silent test loss in test_statistics_functions.py
class TestFinalEnergyByCarrierOil is defined twice in
tests/test_statistics_functions.py:
- Line 368 — 13 test methods, custom
_OilStatisticsAccessor mock
- Line 1150 — 6 test methods, its own separate
_OilStatisticsAccessor mock
Python silently overwrites the first class binding with the second at import time, so
13 test methods never execute. pytest reports green with no warning — this is
exactly the class of risk a "review testing routines" issue should surface, independent
of the coverage percentage. No linter in this repo currently catches redefinition
(no ruff/flake8 config with F811 enabled, no pre-commit hook).
Action: merge the two blocks, keep whichever assertions are non-redundant, and add a
guard (see Phase 0 below) so this can't happen silently again.
Answers to the structural questions
Is the test structure efficient?
Partially. tests/test_statistics_functions.py is one 89 KB file with one TestX class
per statistics function — a reasonable pattern in principle, matching the "each
statistics function is independent" architecture in CLAUDE.md. But nothing enforces
uniqueness of class names within the file, which is how the Oil duplication above went
unnoticed. The file's size also makes review harder than it needs to be.
Would a common mock network across all statistics-function tests be better?
One already exists — tests/conftest.py provides MockPyPSANetwork,
MockStatisticsAccessor, and a mock_network fixture, and most tests use it. The
problem is inconsistent adoption: several test classes (e.g. both
TestFinalEnergyByCarrierOil blocks) hand-roll their own accessor subclass
(_OilStatisticsAccessor) instead of parametrizing the shared one. This is duplicated
setup logic and a contributor to file size. Consolidating onto the shared, parametrized
fixture is the right direction — see Phase 1.
Implementation plan
Target: push class_definitions.py and statistics_functions.py to 95%+, eliminate
duplicate/dead test code, and make mock usage consistent. Written to be picked up
directly by an implementing (AI) agent — each phase is independently shippable and
touches only tests/ (Phase 0–1) or adds tests for existing, unchanged production code
(Phase 2–3).
Phase 0 — Fix the duplicate-class bug (blocking, do first)
- In
tests/test_statistics_functions.py, rename or merge the two
TestFinalEnergyByCarrierOil blocks (lines 368 and 1150). Prefer merging into one
class; de-duplicate assertions that test the same behavior twice, keep the union of
distinct scenarios covered by both _OilStatisticsAccessor variants.
- Run
pixi run pytest tests/test_statistics_functions.py -v and confirm the test count
increases (currently 6 Oil tests run; merged, expect close to 13-19 depending on
dedup) and all pass.
- Add a cheap regression guard: a small test (e.g. in
tests/test_utils.py or a new
tests/test_no_duplicate_test_classes.py) that walks tests/*.py with ast and
fails if any module defines the same top-level class name twice. This is cheaper than
introducing a linter and is self-contained to the test suite.
(Optional, separately proposable: enable ruff with F811 in CI to catch this
category of bug in production code too, not just tests.)
Phase 1 — Consolidate mocks onto shared fixtures
- Audit
tests/test_statistics_functions.py for bespoke accessor subclasses (start
with _OilStatisticsAccessor in both blocks; check other TestX classes for similar
patterns).
- Extend
MockStatisticsAccessor in tests/conftest.py to accept the parameters those
bespoke accessors need (e.g. rescom_empty, all_oil_value, all_oil_empty), either
via constructor kwargs or a small factory fixture
(mock_network_with(**overrides)) so individual test classes stop subclassing.
- Migrate
TestFinalEnergyByCarrierOil (merged from Phase 0) to use the shared,
parametrized fixture instead of _OilStatisticsAccessor. Use this as the template.
- Repeat for any other test classes found in step 1 that define their own accessor.
- Delete the now-dead bespoke mock classes.
- Do not touch
pypsa_validation_processing/statistics_functions.py itself —
this phase is test-only, per the "statistics functions are independent, tests don't
change production behavior" rule.
Phase 2 — Close remaining class_definitions.py gaps (87% → 95%+)
Add targeted unit tests in tests/test_network_processor.py for the currently-missing
lines (re-check exact numbers with --cov-report=term-missing before starting, since
Phase 0/1 changes don't touch this file but line numbers may drift with unrelated
commits):
__init__: FileNotFoundError when network_results_path doesn't exist;
ValueError when definitions_path is unset; FileNotFoundError when
definitions_path doesn't exist; ValueError when model_name/scenario_name is
None.
format_timestamps: the TypeError fallback branch in the pd.to_datetime call.
- The "function not found in statistics_functions.py" warning branch when resolving a
variable to its implementation.
- The multi-unit-parsing branch (
"[" in target_unit and "]" in target_unit) including
both the successful ast.literal_eval path and the exception fallback to "TJ".
- Remaining
KeyError branches around unit/variable lookup in common definitions.
Phase 3 — Close small remaining gaps in statistics_functions.py / workflow.py
statistics_functions.py lines 729-730, 1049 (98% → target ~100%).
workflow.py line 65 (96% → target ~100%).
Phase 4 — Enforce coverage in CI (prevents regression)
- Add
--cov-fail-under=90 (or the value the team agrees on after Phase 2/3 land) to
the test task in pixi.toml so coverage regressions fail CI instead of drifting
silently, the way the original 66%-vs-92% gap in this issue did.
- Confirm
pixi run test and pixi run workflow_test both still pass with the new
threshold.
Definition of done
Review testing routines
Current coverage (as of
main@ 4fc636a, measured withpixi run pytest tests/ --cov=pypsa_validation_processing --cov-report=term-missing)class_definitions.pystatistics_functions.pyutils.pyworkflow.pyThe repo is already well past 80% overall.
class_definitions.pyis the weakest module andthe remaining gaps are narrow — mostly untested error branches (missing-path
FileNotFoundError/ValueErrorchecks in__init__) and one multi-unit-parsing branch,not a systemic hole. The README's
testing-batchcommand referenced in the original bodyno longer exists in
README.md— that item appears already resolved.Critical finding: silent test loss in
test_statistics_functions.pyclass TestFinalEnergyByCarrierOilis defined twice intests/test_statistics_functions.py:_OilStatisticsAccessormock_OilStatisticsAccessormockPython silently overwrites the first class binding with the second at import time, so
13 test methods never execute.
pytestreports green with no warning — this isexactly the class of risk a "review testing routines" issue should surface, independent
of the coverage percentage. No linter in this repo currently catches redefinition
(no ruff/flake8 config with
F811enabled, no pre-commit hook).Action: merge the two blocks, keep whichever assertions are non-redundant, and add a
guard (see Phase 0 below) so this can't happen silently again.
Answers to the structural questions
Is the test structure efficient?
Partially.
tests/test_statistics_functions.pyis one 89 KB file with oneTestXclassper statistics function — a reasonable pattern in principle, matching the "each
statistics function is independent" architecture in
CLAUDE.md. But nothing enforcesuniqueness of class names within the file, which is how the Oil duplication above went
unnoticed. The file's size also makes review harder than it needs to be.
Would a common mock network across all statistics-function tests be better?
One already exists —
tests/conftest.pyprovidesMockPyPSANetwork,MockStatisticsAccessor, and amock_networkfixture, and most tests use it. Theproblem is inconsistent adoption: several test classes (e.g. both
TestFinalEnergyByCarrierOilblocks) hand-roll their own accessor subclass(
_OilStatisticsAccessor) instead of parametrizing the shared one. This is duplicatedsetup logic and a contributor to file size. Consolidating onto the shared, parametrized
fixture is the right direction — see Phase 1.
Implementation plan
Target: push
class_definitions.pyandstatistics_functions.pyto 95%+, eliminateduplicate/dead test code, and make mock usage consistent. Written to be picked up
directly by an implementing (AI) agent — each phase is independently shippable and
touches only
tests/(Phase 0–1) or adds tests for existing, unchanged production code(Phase 2–3).
Phase 0 — Fix the duplicate-class bug (blocking, do first)
tests/test_statistics_functions.py, rename or merge the twoTestFinalEnergyByCarrierOilblocks (lines 368 and 1150). Prefer merging into oneclass; de-duplicate assertions that test the same behavior twice, keep the union of
distinct scenarios covered by both
_OilStatisticsAccessorvariants.pixi run pytest tests/test_statistics_functions.py -vand confirm the test countincreases (currently 6 Oil tests run; merged, expect close to 13-19 depending on
dedup) and all pass.
tests/test_utils.pyor a newtests/test_no_duplicate_test_classes.py) that walkstests/*.pywithastandfails if any module defines the same top-level class name twice. This is cheaper than
introducing a linter and is self-contained to the test suite.
(Optional, separately proposable: enable
ruffwithF811in CI to catch thiscategory of bug in production code too, not just tests.)
Phase 1 — Consolidate mocks onto shared fixtures
tests/test_statistics_functions.pyfor bespoke accessor subclasses (startwith
_OilStatisticsAccessorin both blocks; check otherTestXclasses for similarpatterns).
MockStatisticsAccessorintests/conftest.pyto accept the parameters thosebespoke accessors need (e.g.
rescom_empty,all_oil_value,all_oil_empty), eithervia constructor kwargs or a small factory fixture
(
mock_network_with(**overrides)) so individual test classes stop subclassing.TestFinalEnergyByCarrierOil(merged from Phase 0) to use the shared,parametrized fixture instead of
_OilStatisticsAccessor. Use this as the template.pypsa_validation_processing/statistics_functions.pyitself —this phase is test-only, per the "statistics functions are independent, tests don't
change production behavior" rule.
Phase 2 — Close remaining
class_definitions.pygaps (87% → 95%+)Add targeted unit tests in
tests/test_network_processor.pyfor the currently-missinglines (re-check exact numbers with
--cov-report=term-missingbefore starting, sincePhase 0/1 changes don't touch this file but line numbers may drift with unrelated
commits):
__init__:FileNotFoundErrorwhennetwork_results_pathdoesn't exist;ValueErrorwhendefinitions_pathis unset;FileNotFoundErrorwhendefinitions_pathdoesn't exist;ValueErrorwhenmodel_name/scenario_nameisNone.format_timestamps: theTypeErrorfallback branch in thepd.to_datetimecall.variable to its implementation.
"[" in target_unit and "]" in target_unit) includingboth the successful
ast.literal_evalpath and the exception fallback to"TJ".KeyErrorbranches around unit/variable lookup in common definitions.Phase 3 — Close small remaining gaps in
statistics_functions.py/workflow.pystatistics_functions.pylines 729-730, 1049 (98% → target ~100%).workflow.pyline 65 (96% → target ~100%).Phase 4 — Enforce coverage in CI (prevents regression)
--cov-fail-under=90(or the value the team agrees on after Phase 2/3 land) tothe
testtask inpixi.tomlso coverage regressions fail CI instead of driftingsilently, the way the original 66%-vs-92% gap in this issue did.
pixi run testandpixi run workflow_testboth still pass with the newthreshold.
Definition of done
tests/(Phase 0)class_definitions.pyandstatistics_functions.pyat 95%+ coverageconftest.pyfixtures--cov-fail-underthreshold enforced in thetestpixi taskpixi run testandpixi run workflow_testgreen