Skip to content

Consolidate test helpers and tighten loose assertions#39

Merged
FBumann merged 3 commits intomainfrom
tests/improve
Feb 20, 2026
Merged

Consolidate test helpers and tighten loose assertions#39
FBumann merged 3 commits intomainfrom
tests/improve

Conversation

@FBumann
Copy link
Copy Markdown
Owner

@FBumann FBumann commented Feb 19, 2026

Summary

  • Consolidate duplicated test helpers (_ts, _waste, on-block verification loops) into shared functions in root conftest.py
  • Replace timesteps_3/timesteps_4 fixtures with ts(n) function calls across all test files
  • Replace 12 inline block-verification loops with numpy-based assert_on_blocks()/assert_off_blocks() helpers
  • Tighten ~16 loose directional assertions (> X) with exact expected values where computable
  • Add pythonpath=["tests"] to pytest config for cross-directory imports

Changes

Helpers consolidated into tests/conftest.py

Helper Purpose
ts(n) Create n hourly timesteps starting 2024-01-01
waste(bus) Free-disposal port that absorbs excess on a bus
assert_on_blocks(on, min_length, max_length) Assert on-block durations are within bounds
assert_off_blocks(on, min_length, max_length, skip_leading) Assert off-block durations are within bounds

Assertions tightened

Replaced directional checks (assert obj > X) with exact values in:

  • test_status.py — 6 assertions (objectives: 140, 1020, 200, 140, 240, 120)
  • test_flow_status.py — 1 assertion (60.0)
  • test_combinations.py — 1 assertion (140.0)
  • test_custom.py — 1 assertion (150.0)
  • test_end_to_end.py — 1 assertion (exact formula)
  • test_contributions.py — 4 assertions (5000, 22.6, formula, computed)
  • test_sizing.py — 2 left directional (size not uniquely determined without per-size cost)

Files modified (20)

pyproject.toml, tests/conftest.py, tests/test_data.py, tests/test_types.py, tests/math_port/conftest.py, and 15 test files.

Type of Change

  • Code refactoring

Testing

  • 294 passed, 251 skipped, 0 failed
  • ruff check clean
  • All pre-commit hooks pass

Summary by CodeRabbit

  • Chores

    • Standardized test infrastructure and consolidated helpers for consistent timestep and waste/flow handling across test suites.
    • Replaced many fixture-based patterns with simpler, direct helpers to reduce duplication and streamline test signatures.
  • Tests

    • Updated numerous tests to use the new helpers, tightened numerical assertions, and added on/off-block validation helpers for more deterministic, maintainable tests.

- Add ts(), waste(), assert_on_blocks(), assert_off_blocks() to root conftest.py
- Remove duplicate _ts()/_waste() from 5 files, replace timesteps_3/4 fixtures
- Replace 12 inline block-verification loops with shared numpy-based helpers
- Tighten ~16 directional assertions (> X) with exact expected values
- Add pythonpath=["tests"] to pytest config for cross-directory imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 19, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Centralizes test utilities by adding ts/waste and block-assertion helpers in tests/conftest.py, re-exports them from tests/math_port/conftest.py, removes fixture-based timestep parameters from many tests (replacing with ts(n) calls), and adds tests to pytest pythonpath in pyproject.toml.

Changes

Cohort / File(s) Summary
Configuration
pyproject.toml
Added pythonpath = ["tests"] to pytest config so test helpers are importable during collection.
Shared Test Helpers
tests/conftest.py, tests/math_port/conftest.py
Introduced ts(n) (timesteps), waste(bus) (free-disposal Port), and block validators assert_on_blocks / assert_off_blocks; tests/math_port/conftest.py now re-exports these helpers.
Math tests
tests/math/...
tests/math/test_bus_balance.py, tests/math/test_bus_penalty.py, tests/math/test_contributions.py, tests/math/test_converter.py, tests/math/test_custom.py, tests/math/test_effects.py, tests/math/test_end_to_end.py, tests/math/test_math.py, tests/math/test_sizing.py, tests/math/test_status.py, tests/math/test_storage.py
Removed fixture timestep parameters (e.g., timesteps_3/timesteps_4), updated tests to call ts(n) directly, and replaced manual on/off block checks with assert_on_blocks/assert_off_blocks where applicable.
Math-port tests
tests/math_port/...
tests/math_port/test_combinations.py, tests/math_port/test_effects.py, tests/math_port/test_flow.py, tests/math_port/test_flow_status.py
Replaced _waste with waste; adopted shared ts helper; switched manual block-length checks to assert_on_blocks/assert_off_blocks; updated a few numeric assertions to fixed expected values.
Data & types tests
tests/test_data.py, tests/test_types.py
Replaced fixture-driven timesteps with ts(3)/ts(4) calls in ModelData/test setups and removed local ts fixtures/helpers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 I hopped through timesteps, neat and spry,
I swapped old fixtures for a single cry: ts(n),
Waste ports and block checks at my paw,
Conftest shared — tests now purr and thaw,
A rabbit cheers for tidy tests and gnawed carrot code 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: consolidating duplicated test helpers and tightening loose assertions across the test suite, which aligns with the file-level changes and overall PR objectives.
Description check ✅ Passed The PR description is well-structured and comprehensive, covering all required template sections with clear details about changes, helpers consolidated, assertions tightened, and testing results.
Docstring Coverage ✅ Passed Docstring coverage is 93.13% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tests/improve

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/math/test_status.py (1)

896-900: ⚠️ Potential issue | 🟡 Minor

Local variable ts shadows the module-level import in both half-hour timestep tests

In test_min_uptime_with_half_hour_timesteps (line 898) and test_max_uptime_with_half_hour_timesteps (line 938), a local list ts is assigned and immediately passed to optimize. Within both methods ts now refers to the local list, silently hiding the conftest helper. This works today but will cause a subtle TypeError (calling a list as a function) if someone later writes ts(n) inside either test.

🐛 Proposed fix (applies to both methods)
-        ts = [datetime(2020, 1, 1, h, m) for h in range(4) for m in (0, 30)]
+        timesteps_30min = [datetime(2020, 1, 1, h, m) for h in range(4) for m in (0, 30)]
         result = optimize(
-            ts,
+            timesteps_30min,

And the same rename in the second test at line 938 (range(3)).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/math/test_status.py` around lines 896 - 900, The local variable ts in
both tests (test_min_uptime_with_half_hour_timesteps and
test_max_uptime_with_half_hour_timesteps) shadows the module-level ts helper;
rename the local list (e.g., to ts_list or timestamps_half_hour) and update its
usages passed to optimize so the module-level ts remains callable; ensure you
rename the second occurrence where range(3) is used as well so all local
references are consistent.
🧹 Nitpick comments (5)
tests/test_types.py (1)

9-16: ts fixture is defined but never consumed — consider removing it.

No test method in this file has ts as a parameter; every method that needs a DatetimeIndex constructs one locally (e.g. line 49). The _ts alias import at line 9 exists solely to populate this dead fixture.

♻️ Proposed cleanup
-from conftest import ts as _ts
 ...
-@pytest.fixture
-def ts():
-    return pd.DatetimeIndex(_ts(3))
-
-
 class TestNormalizeTimesteps:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_types.py` around lines 9 - 16, The file defines an unused pytest
fixture named "ts" and an unused import alias "_ts"; remove the dead fixture and
the unused import to clean up the test module: delete the "from conftest import
ts as _ts" import and the "@pytest.fixture def ts(): return
pd.DatetimeIndex(_ts(3))" block so tests continue to create DatetimeIndex
locally (no changes to as_dataarray/compute_dt/normalize_timesteps required);
run the test suite to confirm nothing else depended on the fixture.
tests/conftest.py (2)

8-9: Unnecessary split between TYPE_CHECKING guard and in-function import — prefer a plain top-level import

numpy is a mandatory test dependency, not an optional one. The TYPE_CHECKING guard (line 8–9) combined with the deferred import numpy as np inside _block_lengths (line 29) is the right pattern for optional deps but adds needless indirection here. A single unconditional top-level import is simpler and equivalent.

♻️ Proposed fix
 from __future__ import annotations

 from datetime import datetime
-from typing import TYPE_CHECKING

+import numpy as np

 from fluxopt import Flow, Port

-if TYPE_CHECKING:
-    import numpy as np

Then remove the local import numpy as np on line 29 inside _block_lengths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/conftest.py` around lines 8 - 9, Remove the unnecessary TYPE_CHECKING
guard and make numpy a top-level import: replace the conditional "if
TYPE_CHECKING: import numpy as np" with a normal "import numpy as np" at module
scope, and delete the local "import numpy as np" inside the _block_lengths
function so all uses reference the top-level numpy import.

17-19: waste docstring is missing the Args section

bus is a non-obvious parameter name; an Args entry would document its purpose.

♻️ Proposed fix
 def waste(bus: str) -> Port:
-    """Free-disposal port that absorbs excess on *bus* at zero cost."""
+    """Free-disposal port that absorbs excess on *bus* at zero cost.
+
+    Args:
+        bus: Bus identifier to absorb excess flow from.
+    """
     return Port(f'_waste_{bus}', exports=[Flow(bus=bus)])

As per coding guidelines, public functions with parameters should use Google-style docstrings with an Args section.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/conftest.py` around lines 17 - 19, The docstring for the public
function waste lacks a Google-style Args section describing the bus parameter;
update the waste function's docstring to include an "Args" entry that explains
that bus is the name/identifier of the energy/electricity carrier being absorbed
(e.g., a string representing the bus/commodity) and any expectations (type: str)
and behavior, while keeping the brief description and mention of returned Port
(constructed with Port and Flow) intact.
tests/math/test_contributions.py (2)

197-208: Consider deriving the expected investment cost instead of using a literal

5000.0 is not self-documenting; a reader must mentally trace min_size(50) * effects_per_size(100) to verify it. Since the solver picks min_size as the optimal size (demand is exactly 50 MW), this can be expressed directly:

♻️ Proposed refactor
-        assert grid_inv == pytest.approx(5000.0, abs=1e-6)
+        invest_size = 50.0  # min_size; solver picks minimum to meet 50 MW demand
+        assert grid_inv == pytest.approx(invest_size * 100, abs=1e-6)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/math/test_contributions.py` around lines 197 - 208, Replace the
hard-coded 5000.0 assertion with a derived expected value so the test documents
how the solver arrived at that number: compute expected_investment as min_size *
effects_per_size (e.g., 50 * 100 per the model) then assert grid_inv ==
pytest.approx(expected_investment, abs=1e-6); update the test around
result.effect_contributions(), grid_inv and demand_inv to use the computed
expected value (define min_size and effects_per_size as constants in the test if
they aren’t already) and keep the final sum-vs-solver assertion unchanged.

288-293: Derive 22.6 inline for readability

The tightened assertion replaces a loose check but the magic number 22.6 obscures the two cost components it contains. Expressing the computation explicitly makes the test self-documenting:

♻️ Proposed refactor
-        assert grid_cost == pytest.approx(22.6, abs=1e-6)
+        expected_flow_cost = (50 + 80 + 60) * 0.04   # 7.6
+        expected_running_cost = 3 * 5.0               # 15.0, running all 3 timesteps
+        assert grid_cost == pytest.approx(expected_flow_cost + expected_running_cost, abs=1e-6)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/math/test_contributions.py` around lines 288 - 293, The test uses a
magic number 22.6 for grid_cost; instead derive that expected value by summing
the actual cost components from the contrib dataset so the test is
self-documenting: locate the contrib object and the grid_cost variable and
replace the literal 22.6 with an explicit computation that selects and sums the
two cost parts for contributor='grid(elec)' (e.g., the fixed and running cost
entries) and assert pytest.approx(expected_sum, abs=1e-6) against grid_cost.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/conftest.py`:
- Around line 84-91: The docstring for assert_off_blocks incorrectly states that
"values < 0.5 are 'off'"; update it to match the implementation in
_block_lengths which treats off as "not (value > 0.5)" (i.e., value ≤ 0.5).
Change the on parameter description in assert_off_blocks to say "values ≤ 0.5
are 'off'" (or "<= 0.5" if you prefer ASCII) so the documentation matches the
logic in _block_lengths and any references to skip_leading/on semantics remain
consistent.
- Around line 12-14: The ts(n) helper currently constructs datetimes with
datetime(2024,1,1,h) which raises ValueError for n>24; change ts to build the
sequence by starting at datetime(2024,1,1) and adding
datetime.timedelta(hours=i) for i in range(n) so it supports arbitrarily many
hourly steps, and update the docstring to include a Google-style Args section
documenting the n parameter and return value; locate and modify the ts function
to implement these changes.

---

Outside diff comments:
In `@tests/math/test_status.py`:
- Around line 896-900: The local variable ts in both tests
(test_min_uptime_with_half_hour_timesteps and
test_max_uptime_with_half_hour_timesteps) shadows the module-level ts helper;
rename the local list (e.g., to ts_list or timestamps_half_hour) and update its
usages passed to optimize so the module-level ts remains callable; ensure you
rename the second occurrence where range(3) is used as well so all local
references are consistent.

---

Nitpick comments:
In `@tests/conftest.py`:
- Around line 8-9: Remove the unnecessary TYPE_CHECKING guard and make numpy a
top-level import: replace the conditional "if TYPE_CHECKING: import numpy as np"
with a normal "import numpy as np" at module scope, and delete the local "import
numpy as np" inside the _block_lengths function so all uses reference the
top-level numpy import.
- Around line 17-19: The docstring for the public function waste lacks a
Google-style Args section describing the bus parameter; update the waste
function's docstring to include an "Args" entry that explains that bus is the
name/identifier of the energy/electricity carrier being absorbed (e.g., a string
representing the bus/commodity) and any expectations (type: str) and behavior,
while keeping the brief description and mention of returned Port (constructed
with Port and Flow) intact.

In `@tests/math/test_contributions.py`:
- Around line 197-208: Replace the hard-coded 5000.0 assertion with a derived
expected value so the test documents how the solver arrived at that number:
compute expected_investment as min_size * effects_per_size (e.g., 50 * 100 per
the model) then assert grid_inv == pytest.approx(expected_investment, abs=1e-6);
update the test around result.effect_contributions(), grid_inv and demand_inv to
use the computed expected value (define min_size and effects_per_size as
constants in the test if they aren’t already) and keep the final sum-vs-solver
assertion unchanged.
- Around line 288-293: The test uses a magic number 22.6 for grid_cost; instead
derive that expected value by summing the actual cost components from the
contrib dataset so the test is self-documenting: locate the contrib object and
the grid_cost variable and replace the literal 22.6 with an explicit computation
that selects and sums the two cost parts for contributor='grid(elec)' (e.g., the
fixed and running cost entries) and assert pytest.approx(expected_sum, abs=1e-6)
against grid_cost.

In `@tests/test_types.py`:
- Around line 9-16: The file defines an unused pytest fixture named "ts" and an
unused import alias "_ts"; remove the dead fixture and the unused import to
clean up the test module: delete the "from conftest import ts as _ts" import and
the "@pytest.fixture def ts(): return pd.DatetimeIndex(_ts(3))" block so tests
continue to create DatetimeIndex locally (no changes to
as_dataarray/compute_dt/normalize_timesteps required); run the test suite to
confirm nothing else depended on the fixture.

- Fix ts() to use timedelta (supports n>24), add Args docstrings
- Move numpy to top-level import, remove TYPE_CHECKING guard
- Fix assert_off_blocks docstring: "<= 0.5" matches implementation
- Rename local ts → half_hour_ts to avoid shadowing module-level helper
- Remove unused ts fixture and _ts import from test_types.py
- Replace magic numbers in test_contributions.py with derived expressions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/math/test_status.py (1)

19-65: Optional: tighten the objective assertion to match the docstring's stated total.

The docstring explicitly calculates Total: 50 + 60 = 110, yet no assert_allclose(result.objective, …) is added. Since the cost is fully determined (fixed size, deterministic dispatch), this is straightforward to lock in and consistent with the PR's tightening goal.

✨ Suggested addition
         assert rates[1] >= 60.0 - 1e-5
+        assert_allclose(result.objective, 110.0, rtol=1e-5)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/math/test_status.py` around lines 19 - 65, Add an assertion that the
optimization objective equals the docstring’s computed total (110) to make the
test deterministic: after calling optimize(...) and before finishing the test,
assert that result.objective (or result.objective_value if that API is used) is
approximately 110.0 with a small tolerance (e.g. atol=1e-5) so the test verifies
the total cost stated in the docstring.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/conftest.py`:
- Around line 89-96: The docstring for assert_off_blocks correctly states that
values <= 0.5 are considered "off" and requires no change; leave the docstring
in tests/conftest.py (the assert_off_blocks function) as-is and proceed with
approval/merge.
- Around line 10-17: The ts function correctly generates n hourly timesteps
starting at 2024-01-01 using datetime and timedelta, and the Google-style Args
docstring is present; no code changes are required—leave the ts function (named
ts) and its implementation/Docstring as-is.

---

Nitpick comments:
In `@tests/math/test_status.py`:
- Around line 19-65: Add an assertion that the optimization objective equals the
docstring’s computed total (110) to make the test deterministic: after calling
optimize(...) and before finishing the test, assert that result.objective (or
result.objective_value if that API is used) is approximately 110.0 with a small
tolerance (e.g. atol=1e-5) so the test verifies the total cost stated in the
docstring.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@FBumann FBumann merged commit 666030a into main Feb 20, 2026
5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 10, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants