"""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"" 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()