Files
Plugin_SN_Verfahrensgebiet/tests/test_grenzpunkt_alkis.py
T
DanielandClaude Sonnet 5 84e03d208c Grenzpunkte aus ALKIS werden im ALKIS-Stil angezeigt (#53)
grenzpunkte_gemarkungsweise() ruft nach project.addMapLayer(joined_layer)
jetzt apply_style(joined_layer, "alkis_grenzpunkte.qml") auf - analog zum
bereits etablierten Muster bei Flurstücken/Verfahrensgebiet/BROR in
working_tab.py bzw. verfahrensgebiet_alkis_komplett.py.

Neuer Test test_stil_wird_nach_dem_laden_auf_den_ergebnis_layer_angewendet
mockt die komplette interne WFS-Kette (Extents, Hits, GML-Parsing,
Grenzpunkt-Batch, Join) und prüft, dass apply_style() mit dem Ergebnis-
Layer und dem richtigen Dateinamen aufgerufen wird.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 16:29:19 +02:00

432 lines
16 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.
"""sn_verfahrensgebiet/tests/test_grenzpunkt_alkis.py
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.grenzpunkt_alkis`.
Getestete Szenarien:
Erfolgsfall:
- Vollständiger Workflow: PunktortTA laden → GML-IDs extrahieren →
Grenzpunkte laden → beide Layer zurückgegeben
Fehlerfälle:
- PunktortTA-Layer ungültig → (None, None)
- PunktortTA leer (featureCount == 0) → Warnung + (None, None)
- Keine gehoertZu-Referenzen → (punktort_layer, None) + Warnung
- Grenzpunkt-Layer nach IDs leer → (punktort_layer, None) + Warnung
- Kein Verfahrensgebiet-Layer für BBox-Ableitung → (None, None)
Hilfsfunktionen:
- _extrahiere_gml_ids: xlink:href-Format wird korrekt geparst
- _extrahiere_gml_ids: plain GML-ID ohne Doppelpunkt wird direkt verwendet
- _extrahiere_gml_ids: leeres Feld wird übersprungen
"""
import sys
import types
import unittest
from unittest.mock import patch, MagicMock
# ---------------------------------------------------------------------------
# Minimales QGIS-Mock-Setup
# ---------------------------------------------------------------------------
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_core = types.ModuleType("qgis.core")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace()
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
qgis_mod.core = qgis_core
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.core"] = qgis_core
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import (
grenzpunkt_alkis,
grenzpunkte_gemarkungsweise,
_extrahiere_gml_ids,
)
# ---------------------------------------------------------------------------
# Dummies
# ---------------------------------------------------------------------------
class _DummyRect:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
class _DummyFeature:
def __init__(self, gehoert_zu_val=None):
self._val = gehoert_zu_val
def __getitem__(self, key):
if key == "gehoertZu":
return self._val
raise KeyError(key)
def _make_layer(valid=True, feature_count=3, features=None, name="Layer"):
layer = MagicMock()
layer.isValid.return_value = valid
layer.featureCount.return_value = feature_count if valid else 0
layer.getFeatures.return_value = features or []
layer.name.return_value = name
layer.id.return_value = "LYR1"
return layer
# ---------------------------------------------------------------------------
# Tests: _extrahiere_gml_ids
# ---------------------------------------------------------------------------
class TestExtrahiereGmlIds(unittest.TestCase):
def test_xlink_href_format(self):
"""URN-Format 'urn:adv:oid:DEABC123' → nur 'DEABC123' extrahiert."""
feat = _DummyFeature("urn:adv:oid:DEABC123")
layer = _make_layer(features=[feat])
result = _extrahiere_gml_ids(layer)
self.assertEqual(result, ["DEABC123"])
def test_plain_gml_id(self):
"""Plain-GML-ID ohne Doppelpunkt → direkt übernommen."""
feat = _DummyFeature("DEPLAIN456")
layer = _make_layer(features=[feat])
result = _extrahiere_gml_ids(layer)
self.assertEqual(result, ["DEPLAIN456"])
def test_empty_value_skipped(self):
"""Leerer gehoertZu-Wert wird übersprungen."""
feat = _DummyFeature(None)
layer = _make_layer(features=[feat])
result = _extrahiere_gml_ids(layer)
self.assertEqual(result, [])
def test_deduplication(self):
"""Doppelte Referenzen werden dedupliziert."""
f1 = _DummyFeature("urn:adv:oid:DEDUP001")
f2 = _DummyFeature("urn:adv:oid:DEDUP001")
f3 = _DummyFeature("urn:adv:oid:DEOTHER")
layer = _make_layer(features=[f1, f2, f3])
result = _extrahiere_gml_ids(layer)
self.assertEqual(len(result), 2)
self.assertIn("DEDUP001", result)
self.assertIn("DEOTHER", result)
def test_sorted_output(self):
"""Rückgabe ist aufsteigend sortiert."""
f1 = _DummyFeature("urn:adv:oid:ZZZ")
f2 = _DummyFeature("urn:adv:oid:AAA")
layer = _make_layer(features=[f1, f2])
result = _extrahiere_gml_ids(layer)
self.assertEqual(result, sorted(result))
# ---------------------------------------------------------------------------
# Tests: grenzpunkt_alkis
# ---------------------------------------------------------------------------
_PATCH_PROJECT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsProject"
_PATCH_MSGBOX = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QMessageBox"
_PATCH_WFS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsVectorLayer"
_PATCH_EXTRAHIERE = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._extrahiere_gml_ids"
_PATCH_LADE_GP = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_grenzpunkte_per_ids"
_PATCH_INFO = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.info"
_PATCH_WARNING = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.warning"
class TestGrenzpunktAlkis(unittest.TestCase):
def _proj(self, vg_layer=None):
proj = MagicMock()
proj.mapLayersByName.return_value = [vg_layer] if vg_layer else []
return proj
# ------------------------------------------------------------------ #
# 1. Kein VG-Layer wenn extent=None → (None, None) #
# ------------------------------------------------------------------ #
@patch(_PATCH_MSGBOX)
@patch(_PATCH_PROJECT)
def test_no_vg_layer_returns_none(self, mock_proj, mock_msgbox):
mock_proj.instance.return_value = self._proj(vg_layer=None)
pta, gp = grenzpunkt_alkis(None, extent=None)
self.assertIsNone(pta)
self.assertIsNone(gp)
mock_msgbox.warning.assert_called_once()
# ------------------------------------------------------------------ #
# 2. PunktortTA-Layer ungültig → (None, None) #
# ------------------------------------------------------------------ #
@patch(_PATCH_MSGBOX)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_invalid_punktort_layer(self, mock_proj, mock_wfs, mock_msgbox):
proj = MagicMock()
proj.mapLayersByName.return_value = []
mock_proj.instance.return_value = proj
mock_wfs.return_value = _make_layer(valid=False)
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIsNone(pta)
self.assertIsNone(gp)
mock_msgbox.critical.assert_called_once()
# ------------------------------------------------------------------ #
# 3. PunktortTA leer → Warnung + (None, None) #
# ------------------------------------------------------------------ #
@patch(_PATCH_MSGBOX)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_empty_punktort_layer(self, mock_proj, mock_wfs, mock_msgbox):
mock_proj.instance.return_value = MagicMock()
mock_wfs.return_value = _make_layer(valid=True, feature_count=0)
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIsNone(pta)
self.assertIsNone(gp)
mock_msgbox.warning.assert_called_once()
# ------------------------------------------------------------------ #
# 4. Keine gehoertZu-Referenzen → (punktort_layer, None) + Warnung #
# ------------------------------------------------------------------ #
@patch(_PATCH_WARNING)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_no_gml_ids_returns_pta_only(
self, mock_proj, mock_wfs, mock_extrahiere, mock_warning
):
proj = MagicMock()
mock_proj.instance.return_value = proj
pta_layer = _make_layer(valid=True, feature_count=5)
mock_wfs.return_value = pta_layer
mock_extrahiere.return_value = []
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIs(pta, pta_layer)
self.assertIsNone(gp)
mock_warning.assert_called_once()
proj.addMapLayer.assert_called_once_with(pta_layer)
# ------------------------------------------------------------------ #
# 5. Grenzpunkte-Batch liefert None → (punktort_layer, None) + Warn #
# ------------------------------------------------------------------ #
@patch(_PATCH_WARNING)
@patch(_PATCH_LADE_GP)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_grenzpunkt_batch_empty(
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp, mock_warning
):
proj = MagicMock()
mock_proj.instance.return_value = proj
pta_layer = _make_layer(valid=True, feature_count=3)
mock_wfs.return_value = pta_layer
mock_extrahiere.return_value = ["DE0001", "DE0002"]
mock_lade_gp.return_value = None # Batch leer
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIs(pta, pta_layer)
self.assertIsNone(gp)
mock_warning.assert_called_once()
# ------------------------------------------------------------------ #
# 6. Vollständiger Erfolgsfall #
# ------------------------------------------------------------------ #
@patch(_PATCH_LADE_GP)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_success_returns_both_layers(
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp
):
proj = MagicMock()
mock_proj.instance.return_value = proj
pta_layer = _make_layer(valid=True, feature_count=10, name="PunktortTA")
gp_layer = _make_layer(valid=True, feature_count=10, name="Grenzpunkte")
mock_wfs.return_value = pta_layer
mock_extrahiere.return_value = ["DE001", "DE002", "DE003"]
mock_lade_gp.return_value = gp_layer
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIs(pta, pta_layer)
self.assertIs(gp, gp_layer)
# Beide Layer müssen ins Projekt eingefügt worden sein
calls = [c[0][0] for c in proj.addMapLayer.call_args_list]
self.assertIn(pta_layer, calls)
self.assertIn(gp_layer, calls)
# ------------------------------------------------------------------ #
# 7. Extent aus vorhandenem VG-Layer ableiten #
# ------------------------------------------------------------------ #
@patch(_PATCH_LADE_GP)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_extent_derived_from_vg_layer(
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp
):
"""Wenn kein extent übergeben → Extent aus VG-Layer holen."""
vg_layer = MagicMock()
vg_layer.extent.return_value = _DummyRect()
proj = MagicMock()
proj.mapLayersByName.return_value = [vg_layer]
mock_proj.instance.return_value = proj
pta_layer = _make_layer(valid=True, feature_count=2)
gp_layer = _make_layer(valid=True, feature_count=2)
mock_wfs.return_value = pta_layer
mock_extrahiere.return_value = ["DE999"]
mock_lade_gp.return_value = gp_layer
pta, gp = grenzpunkt_alkis(None, extent=None)
vg_layer.extent.assert_called_once()
self.assertIs(pta, pta_layer)
self.assertIs(gp, gp_layer)
# ---------------------------------------------------------------------------
# Tests: grenzpunkte_gemarkungsweise – Stilanbringung (#53)
# ---------------------------------------------------------------------------
_G_PATCH_EXTENTS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_gemarkung_extents"
_G_PATCH_HITS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._wfs_hits_count"
_G_PATCH_GET_GML = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._wfs_get_gml"
_G_PATCH_PARSE_GML = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._parse_alkis_gml_memory_layer"
_G_PATCH_ERKENNE_FELD = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._erkenne_gehoert_zu_feld"
_G_PATCH_EXTRAHIERE_ID = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._extrahiere_feature_gml_id"
_G_PATCH_LADE_GP_DICT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_grenzpunkte_als_dict"
_G_PATCH_ERSTELLE_JOINED = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._erstelle_joined_layer"
_G_PATCH_WFS_LAYER = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsVectorLayer"
_G_PATCH_PROJECT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsProject"
_G_PATCH_PROGRESS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.create_progress_dialog"
_G_PATCH_APPLY_STIL = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.apply_style"
# QgsField/QgsFeature werden innerhalb der Funktion per lokalem Import geholt
# (from sn_basis.functions import QgsField/QgsFeature) - Patch-Ziel ist daher
# die Quelle, nicht der Aufrufort.
_G_PATCH_QGSFIELD = "sn_basis.functions.QgsField"
_G_PATCH_QGSFEATURE = "sn_basis.functions.QgsFeature"
class _DummyProgress:
def set_value(self, wert): pass
def set_label(self, text): pass
def set_total(self, wert): pass
def is_canceled(self): return False
def close(self): pass
class _DummyBbox:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
class _DummyGmlFeld:
def __init__(self, name):
self._name = name
def name(self):
return self._name
class _DummyGmlFeature:
def __init__(self, gehoert_zu_wert):
self._wert = gehoert_zu_wert
def geometry(self):
return object()
def __getitem__(self, key):
if key == "gehoertZu":
return self._wert
raise KeyError(key)
class _DummyGmlLayer:
"""Minimaler Ersatz für das Ergebnis von _parse_alkis_gml_memory_layer()."""
def __init__(self, feature):
self._feature = feature
def fields(self):
return [_DummyGmlFeld("gehoertZu")]
def getFeatures(self):
return [self._feature]
class TestGrenzpunkteGemarkungsweiseStil(unittest.TestCase):
@patch(_G_PATCH_APPLY_STIL)
@patch(_G_PATCH_QGSFEATURE)
@patch(_G_PATCH_QGSFIELD)
@patch(_G_PATCH_PROGRESS)
@patch(_G_PATCH_PROJECT)
@patch(_G_PATCH_WFS_LAYER)
@patch(_G_PATCH_ERSTELLE_JOINED)
@patch(_G_PATCH_LADE_GP_DICT)
@patch(_G_PATCH_EXTRAHIERE_ID)
@patch(_G_PATCH_ERKENNE_FELD)
@patch(_G_PATCH_PARSE_GML)
@patch(_G_PATCH_GET_GML)
@patch(_G_PATCH_HITS)
@patch(_G_PATCH_EXTENTS)
def test_stil_wird_nach_dem_laden_auf_den_ergebnis_layer_angewendet(
self,
mock_extents,
mock_hits,
mock_get_gml,
mock_parse_gml,
mock_erkenne_feld,
mock_extrahiere_id,
mock_lade_gp_dict,
mock_erstelle_joined,
mock_wfs_layer_cls,
mock_project_cls,
mock_progress,
mock_qgsfield,
mock_qgsfeature,
mock_apply_style,
):
"""Prüft, dass grenzpunkte_gemarkungsweise() nach dem Laden apply_style() mit dem neuen ALKIS-Grenzpunkt-Stil auf den fertigen Join-Layer anwendet (#53)."""
mock_extents.return_value = {"1234": _DummyBbox()}
mock_hits.return_value = 1
mock_get_gml.return_value = b"<fake/>"
mock_parse_gml.return_value = _DummyGmlLayer(_DummyGmlFeature("urn:adv:oid:GP1"))
mock_erkenne_feld.return_value = "gehoertZu"
mock_extrahiere_id.return_value = "PTA1"
mock_lade_gp_dict.return_value = ({"GP1": object()}, ["gp_feld"])
joined_layer = MagicMock()
mock_erstelle_joined.return_value = joined_layer
mock_wfs_layer_cls.return_value = MagicMock()
project = MagicMock()
mock_project_cls.instance.return_value = project
mock_progress.return_value = _DummyProgress()
ergebnis = grenzpunkte_gemarkungsweise(None, ["1234"])
self.assertIs(ergebnis, joined_layer)
project.addMapLayer.assert_called_once_with(joined_layer)
mock_apply_style.assert_called_once_with(joined_layer, "alkis_grenzpunkte.qml")
if __name__ == "__main__":
unittest.main()