From c260ccc2d60cce0e0e9caa5b025c6e0b29e66b8e Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 17 Sep 2026 12:11:13 +0200 Subject: [PATCH] TestFlurstueckAlkis an aktuelle Ladelogik angeglichen, bbox-Doku korrigiert (#16) Test-Audit (Projektwissen #16) deckte auf: tests/test_flurstueck_alkis.py wurde im selben Commit (f8bdcde) angelegt, in dem flurstueck_alkis() von einer einzelnen QgsVectorLayer-Abfrage auf lade_flurstuecke_per_gemarkung() (eine WFS-Anfrage je Gemarkung, Merge in Memory-Layer) umgestellt wurde - die Tests mockten aber weiterhin die alte Ein-Query-Variante und liefen seither ins Leere (8 von 12 Testfaellen rot in der Baseline). - Tests mocken jetzt lade_flurstuecke_per_gemarkung()/_check_existing_ flurstueck_layer() statt QgsVectorLayer direkt, passend zum seit #47 bzw. 944fc82 aktuellen Drei-Knopf-Dialog (Ueberschreiben/Anhaengen/ Abbrechen) und dem Pro-Gemarkung-Ladevorgang. - test_existing_layer_keep_no -> test_existing_layer_abbrechen_returns_none: LoadStatus.KEEP wird von _check_existing_flurstueck_layer() nicht mehr zurueckgegeben, der verbleibende Nicht-Aendern-Pfad ist "Abbrechen". - test_bbox_embedded_in_wfs_uri entfernt: bbox wird von keinem Aufrufer mehr uebergeben und im Code nicht mehr ausgewertet; die vermeintliche URI-Einbettung gab es schon lange nicht mehr. - Modul-Docstring in flurstueck_alkis.py korrigiert: bbox ist reine Rueckwaertskompatibilitaet in der Signatur (Flurstueck-Geometrie kommt aus dem WFS je Gemarkung, keine BBox-Filterung), Memory-Layer statt Direktladung dokumentiert. - Alle Docstrings von TestFlurstueckAlkis auf "Prueft, dass ..." gebracht (Vorgabe docs/leitfaden/06_tests.md) und inhaltlich an die tatsaechlich getestete Logik angepasst; Datei-Docstring ebenfalls korrigiert. - Totes _DummyLayer.extent() entfernt (wird von keiner Aufrufkette mehr erreicht). Kein Verhaltensunterschied in flurstueck_alkis() selbst - nur Tests und Dokumentation an eine bereits bestehende Implementierung angeglichen. Co-Authored-By: Claude Sonnet 5 --- functions/flurstueck_alkis.py | 14 +-- tests/test_flurstueck_alkis.py | 150 ++++++++++++++------------------- 2 files changed, 71 insertions(+), 93 deletions(-) diff --git a/functions/flurstueck_alkis.py b/functions/flurstueck_alkis.py index d16fd97..2ec7aa4 100644 --- a/functions/flurstueck_alkis.py +++ b/functions/flurstueck_alkis.py @@ -3,8 +3,9 @@ Lädt Flurstücke aus dem ALKIS-WFS-Dienst (``adv:AX_Flurstueck``) anhand von Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt. -Der WFS-Layer wird direkt (nicht als Memory-Layer) ins Projekt geladen, -da die einzelnen Features für die spätere Auswahl benötigt werden. +Für jede Gemarkungsnummer wird eine eigene WFS-Anfrage gestellt (umgeht das +WFS-Feature-Limit einer Gesamtabfrage); alle Ergebnisse werden anschließend in +einem gemeinsamen Memory-Layer zusammengeführt. **Erweiterung gegenüber dem Original:** @@ -16,10 +17,11 @@ da die einzelnen Features für die spätere Auswahl benötigt werden. - ``vorgabe_namen``: Zugehörige Bezeichnungen, die im Dialog als Hinweis angezeigt werden (kein eigenständiges Feld, nur Anzeigetext). -- ``bbox``: Optionaler BBox-Parameter, der – sofern übergeben – als - zusätzlicher serverseitiger Filter in die WFS-URI eingebettet wird. So - werden ausschließlich Flurstücke geladen, die den Verfahrensgebiets-Rahmen - überlappen. +- ``bbox``: Nicht mehr verwendet. Die Flurstück-Geometrie kommt ausschließlich + aus dem ALKIS-WFS je Gemarkung; eine BBox filtert höchstens vorgelagert die + Gemarkungsauswahl (siehe ``gemarkung_alkis.py``), nie die Flurstücke selbst. + Der Parameter bleibt nur aus Gründen der Signatur-Rückwärtskompatibilität + erhalten. """ from __future__ import annotations diff --git a/tests/test_flurstueck_alkis.py b/tests/test_flurstueck_alkis.py index 7e018a2..a255a7b 100644 --- a/tests/test_flurstueck_alkis.py +++ b/tests/test_flurstueck_alkis.py @@ -6,8 +6,11 @@ Getestet werden: - Nutzerabbruch bei ask_text → (None, LoadStatus.NONE) - Ungültiger WFS-Layer → (None, LoadStatus.NONE) - Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST -- Bestehender Layer: Nutzer wählt "Neu laden" → LoadStatus.RELOAD -- Bestehender Layer: Nutzer wählt "Behalten" → LoadStatus.KEEP +- Bestehender Layer, Drei-Knopf-Dialog: "Überschreiben" → LoadStatus.RELOAD, + "Abbrechen" → (None, LoadStatus.NONE), "Anhängen" siehe + TestFlurstueckAlkisAnhaengenDedup +- vorgabe_schluessel/vorgabe_namen füllen den Eingabedialog vor +- ohne_schluessel_abfrage=True überspringt die ask_text-Abfrage """ import sys @@ -32,6 +35,7 @@ if "qgis" not in sys.modules: from sn_verfahrensgebiet.functions.flurstueck_alkis import ( flurstueck_alkis, + lade_flurstuecke_per_gemarkung, FLST_LAYER_NAME, FELD_FLURSTUECKSKENNZEICHEN, ) @@ -53,8 +57,8 @@ class _DummyLayer: def id(self): return self._id - def extent(self): - return object() + def featureCount(self): + return 1 class _DummyProject: @@ -82,7 +86,7 @@ class TestFlurstueckAlkis(unittest.TestCase): @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox): - """ask_text → ok=False, text="" → (None, NONE).""" + """Prüft, dass ein Abbruch im Eingabedialog (ok=False) (None, LoadStatus.NONE) liefert.""" mock_ask_text.return_value = ("", False) mock_project.return_value = _DummyProject() @@ -95,7 +99,7 @@ class TestFlurstueckAlkis(unittest.TestCase): @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox): - """ask_text → ok=True, aber Text leer → (None, NONE).""" + """Prüft, dass ein bestätigter, aber leerer Eingabetext ebenfalls (None, LoadStatus.NONE) liefert.""" mock_ask_text.return_value = (" ", True) mock_project.return_value = _DummyProject() @@ -114,7 +118,7 @@ class TestFlurstueckAlkis(unittest.TestCase): def test_invalid_wfs_layer_returns_none( self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox ): - """WFS-Layer nicht valide → (None, NONE).""" + """Prüft, dass ein ungültiger WFS-Layer (None, LoadStatus.NONE) liefert.""" mock_ask_text.return_value = ("1234", True) mock_project.return_value = _DummyProject() mock_wfs_layer_cls.return_value = _DummyLayer(valid=False) @@ -127,67 +131,64 @@ class TestFlurstueckAlkis(unittest.TestCase): # ------------------------------------------------------------------ # # 3. Erfolgreicher Erstladevorgang # # ------------------------------------------------------------------ # - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_success_returns_layer_and_first_status( - self, mock_ask_text, mock_project, mock_wfs_layer_cls + self, mock_ask_text, mock_project, mock_lade ): - """Kein bestehender Layer, WFS valide → (layer, FIRST).""" + """Prüft, dass ohne bestehenden Layer der von lade_flurstuecke_per_gemarkung gelieferte Layer mit LoadStatus.FIRST zurückgegeben wird.""" mock_ask_text.return_value = ("1234, 5678", True) mock_project.return_value = _DummyProject() valid_layer = _DummyLayer(valid=True, layer_id="wfs_ok") - mock_wfs_layer_cls.return_value = valid_layer + mock_lade.return_value = valid_layer layer, status = flurstueck_alkis(None) self.assertIs(layer, valid_layer) self.assertEqual(status, LoadStatus.FIRST) + mock_lade.assert_called_once_with(["1234", "5678"]) @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") - def test_sql_filter_contains_all_schluessel( - self, mock_ask_text, mock_project, mock_wfs_layer_cls - ): - """Der WFS-URI enthält alle eingegebenen Gemarkungsschlüssel.""" - mock_ask_text.return_value = ("1111, 2222, 3333", True) - mock_project.return_value = _DummyProject() - mock_wfs_layer_cls.return_value = _DummyLayer(valid=True) + def test_sql_filter_contains_all_schluessel(self, mock_wfs_layer_cls): + """Prüft, dass lade_flurstuecke_per_gemarkung für jeden Gemarkungsschlüssel eine eigene WFS-Anfrage mit dessen Schlüssel stellt.""" + # Ungültig genügt: nur die aufgebaute URI je Aufruf wird geprüft, das + # tatsächliche Zusammenführen der Features ist nicht Teil dieses Tests. + mock_wfs_layer_cls.return_value = _DummyLayer(valid=False) - flurstueck_alkis(None) + lade_flurstuecke_per_gemarkung(["1111", "2222", "3333"]) - # Erster Positionsarg von QgsVectorLayer ist der URI-String - call_args = mock_wfs_layer_cls.call_args - uri_str = call_args[0][0] if call_args[0] else call_args[1].get("path", "") - self.assertIn("1111", uri_str) - self.assertIn("2222", uri_str) - self.assertIn("3333", uri_str) + uris = [call.args[0] for call in mock_wfs_layer_cls.call_args_list] + self.assertEqual(len(uris), 3) + self.assertIn("1111", uris[0]) + self.assertIn("2222", uris[1]) + self.assertIn("3333", uris[2]) # ------------------------------------------------------------------ # - # 4. Bestehender Layer – Nutzer wählt "Neu laden" # + # 4. Bestehender Layer – Nutzer klickt "Überschreiben" # # ------------------------------------------------------------------ # - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_existing_layer_reload_yes( - self, mock_ask_text, mock_project, mock_msgbox_cls, mock_wfs_layer_cls + self, mock_ask_text, mock_project, mock_msgbox_cls, mock_lade ): - """Bestehender Layer + Nutzer: 'Ja, neu laden' → LoadStatus.RELOAD.""" + """Prüft, dass bei bestehendem Layer und Klick auf 'Überschreiben' der alte Layer entfernt und LoadStatus.RELOAD geliefert wird.""" existing = _DummyLayer(valid=True, layer_id="old_flst") project = _DummyProject(named_layers=[existing]) mock_ask_text.return_value = ("1234", True) mock_project.return_value = project - # StandardButton als int, damit | Operator funktioniert - mock_msgbox_cls.StandardButton.Yes = 0x4000 - mock_msgbox_cls.StandardButton.No = 0x10000 - # QMessageBox.question → Yes - mock_msgbox_cls.question.return_value = 0x4000 + # Drei-Knopf-Dialog: addButton() liefert je Aufruf einen eigenen + # Sentinel, clickedButton() simuliert den Klick auf "Überschreiben". + btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object() + msg_instance = mock_msgbox_cls.return_value + msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen] + msg_instance.clickedButton.return_value = btn_ueberschreiben new_layer = _DummyLayer(valid=True, layer_id="new_flst") - mock_wfs_layer_cls.return_value = new_layer + mock_lade.return_value = new_layer layer, status = flurstueck_alkis(None) @@ -196,46 +197,46 @@ class TestFlurstueckAlkis(unittest.TestCase): self.assertEqual(status, LoadStatus.RELOAD) # ------------------------------------------------------------------ # - # 5. Bestehender Layer – Nutzer wählt "Behalten" # + # 5. Bestehender Layer – Nutzer klickt "Abbrechen" # # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") - def test_existing_layer_keep_no( + def test_existing_layer_abbrechen_returns_none( self, mock_ask_text, mock_project, mock_msgbox_cls ): - """Bestehender Layer + Nutzer: 'Nein, behalten' → LoadStatus.KEEP.""" + """Prüft, dass bei bestehendem Layer und Klick auf 'Abbrechen' nichts verändert wird und (None, LoadStatus.NONE) geliefert wird.""" existing = _DummyLayer(valid=True, layer_id="keep_flst") project = _DummyProject(named_layers=[existing]) mock_ask_text.return_value = ("1234", True) mock_project.return_value = project - # StandardButton als int, damit | Operator funktioniert - mock_msgbox_cls.StandardButton.Yes = 0x4000 - mock_msgbox_cls.StandardButton.No = 0x10000 - # QMessageBox.question → No - mock_msgbox_cls.question.return_value = 0x10000 + btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object() + msg_instance = mock_msgbox_cls.return_value + msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen] + msg_instance.clickedButton.return_value = btn_abbrechen layer, status = flurstueck_alkis(None) - self.assertIs(layer, existing) - self.assertEqual(status, LoadStatus.KEEP) + self.assertIsNone(layer) + self.assertEqual(status, LoadStatus.NONE) self.assertEqual(project.removed, []) # nichts gelöscht + mock_ask_text.assert_not_called() # Abbruch schon vor der Schlüsselabfrage # ------------------------------------------------------------------ # # 6. vorgabe_schluessel füllt ask_text-Default vor # # ------------------------------------------------------------------ # - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_vorgabe_schluessel_prefills_default_text( - self, mock_ask_text, mock_project, mock_wfs_layer_cls + self, mock_ask_text, mock_project, mock_lade ): - """vorgabe_schluessel=['1234','5678'] → ask_text erhält '1234, 5678' als default_text.""" + """Prüft, dass vorgabe_schluessel als vorbelegter default_text im Eingabedialog erscheint.""" mock_ask_text.return_value = ("1234, 5678", True) mock_project.return_value = _DummyProject() - mock_wfs_layer_cls.return_value = _DummyLayer(valid=True) + mock_lade.return_value = _DummyLayer(valid=True) flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"]) @@ -249,16 +250,16 @@ class TestFlurstueckAlkis(unittest.TestCase): # ------------------------------------------------------------------ # # 7. vorgabe_namen erscheint im Label-Text # # ------------------------------------------------------------------ # - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_vorgabe_namen_appears_in_label( - self, mock_ask_text, mock_project, mock_wfs_layer_cls + self, mock_ask_text, mock_project, mock_lade ): - """vorgabe_namen=['Musterstadt'] → ask_text-label enthält den Namen.""" + """Prüft, dass vorgabe_namen im Beschriftungstext des Eingabedialogs erscheint.""" mock_ask_text.return_value = ("9999", True) mock_project.return_value = _DummyProject() - mock_wfs_layer_cls.return_value = _DummyLayer(valid=True) + mock_lade.return_value = _DummyLayer(valid=True) flurstueck_alkis(None, vorgabe_namen=["Musterstadt"]) @@ -268,43 +269,18 @@ class TestFlurstueckAlkis(unittest.TestCase): self.assertIn("Musterstadt", label_arg) # ------------------------------------------------------------------ # - # 8. bbox wird in WFS-URI eingebettet # + # 8. Rückwärtskompatibilität: alle neuen Params optional # # ------------------------------------------------------------------ # - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") - def test_bbox_embedded_in_wfs_uri( - self, mock_ask_text, mock_project, mock_wfs_layer_cls - ): - """bbox-Parameter → WFS-URI enthält 'bbox=' Substring.""" - mock_ask_text.return_value = ("1234", True) - mock_project.return_value = _DummyProject() - mock_wfs_layer_cls.return_value = _DummyLayer(valid=True) - - class _Rect: - def xMinimum(self): return 414000.0 - def yMinimum(self): return 5657000.0 - def xMaximum(self): return 415000.0 - def yMaximum(self): return 5658000.0 - - flurstueck_alkis(None, bbox=_Rect()) - - uri_str = mock_wfs_layer_cls.call_args[0][0] - self.assertIn("bbox=", uri_str.lower()) - - # ------------------------------------------------------------------ # - # 9. Rückwärtskompatibilität: alle neuen Params optional # - # ------------------------------------------------------------------ # - @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer") + @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") def test_backwards_compatible_no_new_params( - self, mock_ask_text, mock_project, mock_wfs_layer_cls + self, mock_ask_text, mock_project, mock_lade ): - """flurstueck_alkis(tab) ohne neue Parameter → kein Fehler.""" + """Prüft, dass flurstueck_alkis() ohne die neuen optionalen Parameter weiterhin fehlerfrei einen Layer mit LoadStatus.FIRST liefert.""" mock_ask_text.return_value = ("0000", True) mock_project.return_value = _DummyProject() - mock_wfs_layer_cls.return_value = _DummyLayer(valid=True) + mock_lade.return_value = _DummyLayer(valid=True) layer, status = flurstueck_alkis(None) @@ -312,7 +288,7 @@ class TestFlurstueckAlkis(unittest.TestCase): self.assertEqual(status, LoadStatus.FIRST) # ------------------------------------------------------------------ # - # 10. ohne_schluessel_abfrage: vorgabe_schluessel direkt, kein ask_text (#47) + # 9. ohne_schluessel_abfrage: vorgabe_schluessel direkt, kein ask_text (#47) # ------------------------------------------------------------------ # @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @@ -320,7 +296,7 @@ class TestFlurstueckAlkis(unittest.TestCase): def test_ohne_schluessel_abfrage_ueberspringt_ask_text( self, mock_ask_text, mock_project, mock_lade ): - """ohne_schluessel_abfrage=True verwendet vorgabe_schluessel direkt, ohne ask_text aufzurufen.""" + """Prüft, dass ohne_schluessel_abfrage=True vorgabe_schluessel direkt verwendet und ask_text nicht aufgerufen wird.""" mock_project.return_value = _DummyProject() geladen = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")]) mock_lade.return_value = geladen