Skip to content

Test Plan: 252 Endpoints

Problem

Business logic calculations are inline in the get_results_summary and get_validated_totals routes. They cannot be unit tested. They must first be extracted into pure functions.


Step 1 — Extract calculations (prerequisite refactor)

Create backend/app/utils/report_computations.py

Extract two pure functions (follows existing pattern: utils/emission_breakdown.py):

def compute_validated_totals(
    emission_stats: dict[str, float],
    fte_stats: dict[str, float],
    headcount_type_id: str,
) -> dict:
    ...

def compute_results_summary(
    current_emissions: dict[str, float | None],
    current_fte: dict[str, float | None],
    prev_emissions: dict[str, float],
    co2_per_km_kg: float,
    headcount_key: str,
) -> dict:
    ...

Update carbon_report_module_stats.py

Routes become simple dispatchers:

from app.utils.report_computations import compute_validated_totals, compute_results_summary

# get_validated_totals route:
emission_stats = await DataEntryEmissionService(db).get_stats_by_carbon_report_id(...)
fte_stats = await DataEntryService(db).get_stats_by_carbon_report_id(...)
return compute_validated_totals(emission_stats, fte_stats, str(ModuleTypeEnum.headcount.value))

# get_results_summary route:
raw = await UnitTotalsService(db).get_results_summary(carbon_report_id)
return compute_results_summary(
    raw["current_emissions"],
    raw["current_fte"],
    raw["prev_emissions"],
    get_settings().CO2_PER_KM_KG,
    str(ModuleTypeEnum.headcount.value),
)

Step 2 — Test inventory

Testing approach

Tests use realistic multi-module fixtures that cover many edge cases in a single scenario, complemented by targeted edge-case tests for div/0 and None guards. This avoids trivial tests (e.g. "does dividing by 1000 work?") while giving strong coverage on the logic that actually breaks.


2a. compute_validated_totals() — pure, no DB

File: backend/tests/unit/utils/test_compute_validated_totals.py

Fixture: realistic multi-module scenario

emission = {"1": 5000.0, "2": 25000.0, "4": 15000.0, "7": 3200.0}
fte = {"1": 120.0}
headcount_type_id = "1"

Assertions on single call:

What Expected
modules[1] 120.0 (FTE wins for headcount module)
modules[2] 25.0 (25000 kg → 25.0 tonnes)
modules[4] 15.0
modules[7] 3.2
module key order sorted by int: [1, 2, 4, 7]
total_tonnes_co2eq 48.2 (5000+25000+15000+3200)/1000
total_fte 120.0

Edge-case tests (parametrized)

ID Input Expected
both_empty emission={}, fte={} modules={}, total_tonnes=0.0, total_fte=0.0
zero_emission emission={"4": 0.0, "2": 1000.0}, fte={} modules={4:0.0, 2:1.0}, total_tonnes=1.0
zero_fte emission={}, fte={"1": 0.0} modules={1: 0.0}, total_fte=0.0
headcount_no_fte emission={"1": 8000.0}, fte={} modules[1]==8.0 (falls back to emission/1000)
fte_only emission={}, fte={"1": 50.0} modules={1: 50.0}, total_fte=50.0, total_tonnes=0.0

2b. compute_results_summary() — pure, no DB

File: backend/tests/unit/utils/test_compute_results_summary.py

Fixture: realistic multi-module scenario

current_emissions = {
    "1": 5000.0,     # headcount — has FTE, has prev (went down)
    "2": 12000.0,    # travel — has prev (unchanged)
    "4": 8500.0,     # equipment — prev == 0 (div/0 guard)
    "5": None,       # purchases — not validated → skipped
    "7": 3200.0,     # cloud — no prev at all
}
current_fte = {"1": 120.0}
prev_emissions = {"1": 6000.0, "2": 12000.0, "4": 0.0}
co2_per_km_kg = 0.17
headcount_key = "1"

Assertions on module_results:

module total_tonnes total_fte year_comparison_% equivalent_car_km prev_tonnes
1 5.0 120.0 -16.67 (5000−6000)/6000 5000/0.17 6.0
2 12.0 None 0.0 (unchanged) 12000/0.17 12.0
4 8.5 None None (prev==0, div/0) 8500/0.17 0.0
5 — (skipped)
7 3.2 None None (no prev) 3200/0.17 None

Assertions on unit_totals:

Field Expected
total_tonnes_co2eq 28.7 ((5000+12000+8500+3200)/1000)
total_fte 120.0
tonnes_co2eq_per_fte 28.7 / 120.0
equivalent_car_km 28700 / 0.17
prev_total_tonnes 18.0 ((6000+12000+0)/1000)
year_comparison_% +59.44 ((28700−18000)/18000×100)
co2_per_km_kg 0.17

Edge-case tests (targeted)

ID Input override What it tests
empty_current current_emissions={} total_tonnes is None (not 0.0)
empty_prev prev_emissions={} prev_total is None, year_comparison is None
prev_all_zero prev_emissions={"1": 0.0, "2": 0.0} year_comparison is None (total prev==0)
no_fte current_fte={} (no headcount) total_fte is None, tonnes_per_fte is None
fte_zero current_fte={"1": 0.0} tonnes_per_fte is None (div/0)
single_module_decrease curr={"2": 4500.0}, prev={"2": 5000.0} year_comparison == -10.0
single_module_full_drop curr={"2": 0.0}, prev={"2": 5000.0} year_comparison == -100.0

2c. Repository tests (DB, SQL filtering)

Add to test_data_entry_emission_repo.py:

get_validated_totals_by_unit()

Test Verifies
basic 1 year, 1 validated module → correct result
multi-year 2 years → list ordered ASC
sums modules 2 validated modules same year → sum of both
excludes IN_PROGRESS IN_PROGRESS module → not counted
excludes other unit 2 units → only the correct unit
no data []

get_stats_by_carbon_report_id() (emission repo)

Test Verifies
single validated module {"module_type_id_str": sum_kg}
multi modules 2 validated modules → 2 keys
excludes IN_PROGRESS → absent from dict
excludes other carbon_report → no leakage between reports
empty {}

Add to test_data_entry_repo.py:

get_stats_by_carbon_report_id() (FTE)

Test Verifies
basic DataEntry.data["fte"]=25.5{"1": 25.5}
multiple entries FTE summed
non-validated module {}
data without "fte" key → absent from result
empty {}

2d. Service tests (orchestration)

Create backend/tests/unit/services/test_unit_totals_service.py:

Test Verifies
returned structure keys {current_emissions, current_fte, prev_emissions} present
no previous report prev_emissions == {}
previous report exists prev_emissions non-empty
carbon_report not found raises ValueError
non-validated module absent from current_emissions
yearly — basic format [{"year", "kg_co2eq"}]
yearly — empty []

Files

File Action
backend/app/utils/report_computations.py Create with compute_validated_totals() + compute_results_summary()
backend/app/api/v1/carbon_report_module_stats.py Refactor routes to call utils/report_computations
backend/tests/unit/utils/__init__.py Create (empty)
backend/tests/unit/utils/test_compute_validated_totals.py Create
backend/tests/unit/utils/test_compute_results_summary.py Create
backend/tests/unit/repositories/test_data_entry_emission_repo.py Add 2 sections
backend/tests/unit/repositories/test_data_entry_repo.py Add 1 section
backend/tests/unit/services/test_unit_totals_service.py Create

Run

cd backend && python -m pytest \
  tests/unit/utils/ \
  tests/unit/services/test_unit_totals_service.py \
  tests/unit/repositories/test_data_entry_emission_repo.py \
  tests/unit/repositories/test_data_entry_repo.py \
  -v