gwoe-antragspruefer/tests/test_auswertungen.py
Dotty Dotter 698562b1f5 test(#134): Coverage-Backfill auswertungen + Repositories
- app/auswertungen.py 87.4% → 97.9%
  - TestLoadAssessmentsRobustness: ungueltiges JSON in fraktionen-Spalte
    fallback to []
  - TestAggregateMatrixSkipsBlanks: bundesland-NULL-Eintrag wird ignoriert
  - TestGetWahlperioden: sortierte Liste

- app/repositories/abonnement_repository.py 85.2% → 100%
- app/repositories/antrag_repository.py 87.0% → 98.1%
- app/repositories/bewertung_repository.py 90% → 100%

Pattern fuer Sqlite-Repos: AsyncMock auf database.X-Funktion, dann
pruefen dass die Methode korrekt delegiert (Argumente, Return-Wert).
Trivial wrappers, aber jetzt auditierbar.

Total: 48.7% → 49.2%, 686 → 705 Tests.
2026-04-28 10:54:28 +02:00

315 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests für app.wahlperioden und app.auswertungen.
Issue #58 + Roadmap #59 Phase C. Verifiziert die Aggregations-Logik
gegen eine in-memory SQLite-DB mit kontrollierten Sample-Assessments.
"""
from __future__ import annotations
import json
import sqlite3
from datetime import datetime
from pathlib import Path
import pytest
from app.auswertungen import (
aggregate_matrix,
aggregate_zeitreihe,
export_long_format,
)
from app.wahlperioden import all_wahlperioden, wahlperiode_for
# ─────────────────────────────────────────────────────────────────────────────
# wahlperioden helper
# ─────────────────────────────────────────────────────────────────────────────
class TestWahlperiodeFor:
def test_current_wp_for_recent_date(self):
assert wahlperiode_for("2026-03-18", "MV") == "MV-WP8"
def test_previous_wp_for_old_date(self):
# MV WP8 startete am 26.10.2021 — alles davor ist WP7
assert wahlperiode_for("2020-01-01", "MV") == "MV-WP7"
def test_unknown_bl_returns_none(self):
assert wahlperiode_for("2026-01-01", "XX") is None
def test_empty_datum_returns_current_wp(self):
# Wenn kein Datum bekannt → wir nehmen die aktuelle WP an,
# weil das die einzig sinnvolle Default-Annahme ist
assert wahlperiode_for("", "NRW") == "NRW-WP18"
def test_all_wahlperioden_lists_each_bl_twice(self):
out = all_wahlperioden()
# 16 Bundesländer + BUND × 2 WPs = 34 Einträge (#56 fügt BUND hinzu)
assert len(out) == 34
# Aktuelle und vorherige WP für NRW
assert "NRW-WP18" in out
assert "NRW-WP17" in out
# BUND ist auch dabei
assert "BUND-WP21" in out
# ─────────────────────────────────────────────────────────────────────────────
# Test-DB-Fixture
# ─────────────────────────────────────────────────────────────────────────────
@pytest.fixture
def sample_db(tmp_path: Path) -> Path:
"""Lege eine Mini-Assessments-DB an, die typische Fälle abdeckt."""
db = tmp_path / "test_assessments.db"
conn = sqlite3.connect(str(db))
conn.execute("""
CREATE TABLE assessments (
drucksache TEXT PRIMARY KEY,
title TEXT,
fraktionen TEXT,
datum TEXT,
bundesland TEXT,
gwoe_score REAL,
link TEXT,
gwoe_begruendung TEXT,
gwoe_matrix TEXT,
gwoe_schwerpunkt TEXT,
wahlprogramm_scores TEXT,
verbesserungen TEXT,
staerken TEXT,
schwaechen TEXT,
empfehlung TEXT,
empfehlung_symbol TEXT,
verbesserungspotenzial TEXT,
themen TEXT,
antrag_zusammenfassung TEXT,
antrag_kernpunkte TEXT,
source TEXT,
model TEXT,
created_at TEXT,
updated_at TEXT
)
""")
samples = [
# NRW WP18 — drei Anträge, zwei Parteien
("18/100", "NRW", "2024-01-15", '["CDU"]', 7.0),
("18/101", "NRW", "2024-02-15", '["SPD"]', 8.0),
("18/102", "NRW", "2024-03-15", '["CDU"]', 5.0),
# MV WP8 — Koalitionsantrag (zwei Parteien zählen beide)
("8/200", "MV", "2024-04-01", '["SPD","LINKE"]', 6.0),
("8/201", "MV", "2025-01-10", '["AfD"]', 2.0),
# MV WP7 — historischer Antrag vor wahlperiode_start (2021-10-26)
("7/100", "MV", "2020-05-01", '["CDU"]', 4.0),
# BB — FREIE WÄHLER soll als BVB-FW kanonisiert werden
("8/2", "BB", "2024-10-17", '["FREIE WÄHLER"]', 6.5),
]
now = datetime.utcnow().isoformat()
for ds, bl, dat, fr, sc in samples:
conn.execute(
"INSERT INTO assessments (drucksache, title, fraktionen, datum, bundesland, "
"gwoe_score, source, model, created_at, updated_at) VALUES "
"(?, ?, ?, ?, ?, ?, 'test', 'test', ?, ?)",
(ds, f"Test {ds}", fr, dat, bl, sc, now, now),
)
conn.commit()
conn.close()
return db
# ─────────────────────────────────────────────────────────────────────────────
# aggregate_matrix
# ─────────────────────────────────────────────────────────────────────────────
class TestAggregateMatrix:
def test_total_count(self, sample_db):
m = aggregate_matrix(db_path=sample_db)
assert m["total"] == 7
def test_bundeslaender_listed(self, sample_db):
m = aggregate_matrix(db_path=sample_db)
assert set(m["bundeslaender"]) == {"NRW", "MV", "BB"}
def test_nrw_cdu_average(self, sample_db):
# NRW-CDU: 7.0 + 5.0 → Avg 6.0, n=2
m = aggregate_matrix(db_path=sample_db)
cell = m["cells"]["NRW"]["CDU"]
assert cell["n"] == 2
assert cell["avg"] == 6.0
def test_koalition_counts_both_parties(self, sample_db):
# MV-SPD und MV-LINKE bekommen beide den Score 6.0 (n=1)
m = aggregate_matrix(db_path=sample_db)
assert m["cells"]["MV"]["SPD"]["n"] == 1
assert m["cells"]["MV"]["LINKE"]["n"] == 1
assert m["cells"]["MV"]["SPD"]["avg"] == 6.0
def test_filter_by_wahlperiode(self, sample_db):
# NRW-WP18-Filter → nur die 3 NRW-Anträge
m = aggregate_matrix(filter_wp="NRW-WP18", db_path=sample_db)
assert m["total"] == 3
assert set(m["bundeslaender"]) == {"NRW"}
def test_filter_excludes_old_wp(self, sample_db):
# MV-WP8 darf den 7/100-Antrag (datum=2020) NICHT enthalten
m = aggregate_matrix(filter_wp="MV-WP8", db_path=sample_db)
assert m["total"] == 2 # nur 8/200 und 8/201
# CDU darf NICHT vorkommen, weil der CDU-Antrag in WP7 war
assert "CDU" not in m["cells"].get("MV", {})
def test_bb_freie_waehler_normalized_to_bvb(self, sample_db):
# Die BB-FW-Drucksache muss als BVB-FW gezählt werden, NICHT als
# generisches FREIE WÄHLER — das ist der eigentliche Mehrwert
# des Parteinamen-Mappers (#55)
m = aggregate_matrix(db_path=sample_db)
bb_cells = m["cells"]["BB"]
assert "BVB-FW" in bb_cells
assert bb_cells["BVB-FW"]["n"] == 1
assert "FREIE WÄHLER" not in bb_cells
def test_empty_db_returns_empty_matrix(self, tmp_path):
m = aggregate_matrix(db_path=tmp_path / "missing.db")
assert m["total"] == 0
assert m["bundeslaender"] == []
# ─────────────────────────────────────────────────────────────────────────────
# aggregate_zeitreihe
# ─────────────────────────────────────────────────────────────────────────────
class TestAggregateZeitreihe:
def test_mv_cdu_two_wps(self, sample_db):
# MV-CDU hat einen Eintrag in WP7 (4.0) und keinen in WP8
z = aggregate_zeitreihe("MV", "CDU", db_path=sample_db)
wps = {entry["wp"]: entry for entry in z["wahlperioden"]}
assert "MV-WP7" in wps
assert wps["MV-WP7"]["avg"] == 4.0
assert wps["MV-WP7"]["n"] == 1
def test_nrw_cdu_one_wp(self, sample_db):
z = aggregate_zeitreihe("NRW", "CDU", db_path=sample_db)
assert len(z["wahlperioden"]) == 1
assert z["wahlperioden"][0]["avg"] == 6.0
def test_unknown_combination_empty(self, sample_db):
z = aggregate_zeitreihe("NRW", "AfD", db_path=sample_db)
assert z["wahlperioden"] == []
# ─────────────────────────────────────────────────────────────────────────────
# export_long_format
# ─────────────────────────────────────────────────────────────────────────────
class TestExportLongFormat:
def test_csv_has_header(self, sample_db):
csv_text = export_long_format(db_path=sample_db)
first_line = csv_text.splitlines()[0]
assert "drucksache" in first_line
assert "bundesland" in first_line
assert "wahlperiode" in first_line
assert "partei" in first_line
assert "gwoe_score" in first_line
def test_koalition_yields_two_rows(self, sample_db):
csv_text = export_long_format(db_path=sample_db)
lines = csv_text.splitlines()[1:] # ohne Header
# 8/200 ist Koalitionsantrag (SPD+LINKE) → 2 Zeilen
mv_8_200_lines = [l for l in lines if l.startswith("8/200,")]
assert len(mv_8_200_lines) == 2
def test_bb_fw_normalized_in_csv(self, sample_db):
csv_text = export_long_format(db_path=sample_db)
assert "BVB-FW" in csv_text
# Generic FREIE WÄHLER darf in der Zeile NICHT auftauchen
bb_lines = [l for l in csv_text.splitlines() if "BB" in l and "8/2," in l]
assert any("BVB-FW" in l for l in bb_lines)
# ─────────────────────────────────────────────────────────────────────────────
# Edge-Cases (#134 Coverage-Backfill)
# ─────────────────────────────────────────────────────────────────────────────
class TestLoadAssessmentsRobustness:
"""_load_assessments toleriert kaputte JSON-Eintraege im fraktionen-Feld."""
def test_invalid_json_in_fraktionen_falls_back_to_empty(self, tmp_path):
from app.auswertungen import _load_assessments
db = tmp_path / "broken.db"
conn = sqlite3.connect(str(db))
conn.execute("""
CREATE TABLE assessments (
drucksache TEXT PRIMARY KEY, title TEXT,
fraktionen TEXT, datum TEXT, bundesland TEXT,
gwoe_score REAL, link TEXT, gwoe_begruendung TEXT,
gwoe_matrix TEXT, gwoe_schwerpunkt TEXT,
wahlprogramm_scores TEXT, verbesserungen TEXT,
staerken TEXT, schwaechen TEXT, empfehlung TEXT,
empfehlung_symbol TEXT, verbesserungspotenzial TEXT,
themen TEXT, antrag_zusammenfassung TEXT,
antrag_kernpunkte TEXT, source TEXT, model TEXT,
created_at TEXT, updated_at TEXT
)
""")
# fraktionen-Feld enthaelt kein gueltiges JSON
conn.execute(
"INSERT INTO assessments (drucksache, bundesland, datum, fraktionen, gwoe_score) "
"VALUES (?, ?, ?, ?, ?)",
("18/777", "NRW", "2024-01-01", "{not json", 5.0),
)
conn.commit()
conn.close()
rows = _load_assessments(db)
assert len(rows) == 1
assert rows[0]["fraktionen"] == [] # Fallback
class TestAggregateMatrixSkipsBlanks:
def test_skips_assessments_without_bundesland(self, tmp_path):
"""Anträge ohne bundesland werden ignoriert (continue-Branch line 115)."""
from app.auswertungen import aggregate_matrix
db = tmp_path / "blanks.db"
conn = sqlite3.connect(str(db))
conn.execute("""
CREATE TABLE assessments (
drucksache TEXT PRIMARY KEY, title TEXT,
fraktionen TEXT, datum TEXT, bundesland TEXT,
gwoe_score REAL, link TEXT, gwoe_begruendung TEXT,
gwoe_matrix TEXT, gwoe_schwerpunkt TEXT,
wahlprogramm_scores TEXT, verbesserungen TEXT,
staerken TEXT, schwaechen TEXT, empfehlung TEXT,
empfehlung_symbol TEXT, verbesserungspotenzial TEXT,
themen TEXT, antrag_zusammenfassung TEXT,
antrag_kernpunkte TEXT, source TEXT, model TEXT,
created_at TEXT, updated_at TEXT
)
""")
conn.execute(
"INSERT INTO assessments (drucksache, bundesland, datum, fraktionen, gwoe_score) "
"VALUES (?, ?, ?, ?, ?)",
("X/1", None, "2024-01-01", '["CDU"]', 7.0), # bundesland NULL
)
conn.execute(
"INSERT INTO assessments (drucksache, bundesland, datum, fraktionen, gwoe_score) "
"VALUES (?, ?, ?, ?, ?)",
("18/1", "NRW", "2024-01-01", '["CDU"]', 7.0),
)
conn.commit()
conn.close()
m = aggregate_matrix(db_path=db)
assert m["total"] == 1 # nur der NRW-Eintrag
assert m["bundeslaender"] == ["NRW"]
class TestGetWahlperioden:
def test_returns_sorted_list(self, sample_db):
from app.auswertungen import get_wahlperioden
wps = get_wahlperioden(db_path=sample_db)
assert wps == sorted(wps)
# Sample-DB enthaelt NRW-WP18, MV-WP8, MV-WP7 sowie BB-WP8
assert any("NRW" in w for w in wps)