Files
Plugin_SN_Verfahrensgebiet/tests/test_flurstueck_alkis.py
T
DanielandClaude Sonnet 5 821bb0640e Doppeltes Laden von Flurstuecken beim Anhaengen verhindert (#28)
Beim Anhaengen an einen bestehenden Flurstueck-Layer wurden alle neu
geladenen Objekte unabhaengig davon hinzugefuegt, ob sie ueber
flurstueckskennzeichen bereits im Layer vorhanden waren. Jetzt werden
nur noch neue Kennzeichen angehaengt; fehlt das Feld, bleibt das
bisherige Verhalten als Rueckfall erhalten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 08:48:14 +02:00

428 lines
18 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_flurstueck_alkis.py
Unit-Tests für das flurstueck_alkis-Modul.
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
"""
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.flurstueck_alkis import (
flurstueck_alkis,
FLST_LAYER_NAME,
FELD_FLURSTUECKSKENNZEICHEN,
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
# ---------------------------------------------------------------------------
# Dummies
# ---------------------------------------------------------------------------
class _DummyLayer:
def __init__(self, valid: bool, layer_id="L1"):
self._valid = valid
self._id = layer_id
def isValid(self):
return self._valid
def id(self):
return self._id
def extent(self):
return object()
class _DummyProject:
def __init__(self, named_layers=None):
self._named_layers = named_layers or []
self.removed = []
def mapLayersByName(self, name):
return self._named_layers
def removeMapLayer(self, lid):
self.removed.append(lid)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestFlurstueckAlkis(unittest.TestCase):
# ------------------------------------------------------------------ #
# 1. Nutzerabbruch (ask_text liefert ok=False / None) #
# ------------------------------------------------------------------ #
@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_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
"""ask_text → ok=False, text="" → (None, NONE)."""
mock_ask_text.return_value = ("", False)
mock_project.return_value = _DummyProject()
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
@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_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
"""ask_text → ok=True, aber Text leer → (None, NONE)."""
mock_ask_text.return_value = (" ", True)
mock_project.return_value = _DummyProject()
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
# ------------------------------------------------------------------ #
# 2. Ungültiger WFS-Layer #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@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_invalid_wfs_layer_returns_none(
self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox
):
"""WFS-Layer nicht valide → (None, NONE)."""
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
# ------------------------------------------------------------------ #
# 3. Erfolgreicher Erstladevorgang #
# ------------------------------------------------------------------ #
@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_success_returns_layer_and_first_status(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""Kein bestehender Layer, WFS valide → (layer, FIRST)."""
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
layer, status = flurstueck_alkis(None)
self.assertIs(layer, valid_layer)
self.assertEqual(status, LoadStatus.FIRST)
@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)
flurstueck_alkis(None)
# 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)
# ------------------------------------------------------------------ #
# 4. Bestehender Layer – Nutzer wählt "Neu laden" #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@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
):
"""Bestehender Layer + Nutzer: 'Ja, neu laden' → LoadStatus.RELOAD."""
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
new_layer = _DummyLayer(valid=True, layer_id="new_flst")
mock_wfs_layer_cls.return_value = new_layer
layer, status = flurstueck_alkis(None)
self.assertIn(existing.id(), project.removed)
self.assertIs(layer, new_layer)
self.assertEqual(status, LoadStatus.RELOAD)
# ------------------------------------------------------------------ #
# 5. Bestehender Layer – Nutzer wählt "Behalten" #
# ------------------------------------------------------------------ #
@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(
self, mock_ask_text, mock_project, mock_msgbox_cls
):
"""Bestehender Layer + Nutzer: 'Nein, behalten' → LoadStatus.KEEP."""
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
layer, status = flurstueck_alkis(None)
self.assertIs(layer, existing)
self.assertEqual(status, LoadStatus.KEEP)
self.assertEqual(project.removed, []) # nichts gelöscht
# ------------------------------------------------------------------ #
# 6. vorgabe_schluessel füllt ask_text-Default vor #
# ------------------------------------------------------------------ #
@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_vorgabe_schluessel_prefills_default_text(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""vorgabe_schluessel=['1234','5678'] → ask_text erhält '1234, 5678' als default_text."""
mock_ask_text.return_value = ("1234, 5678", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"])
args, kwargs = mock_ask_text.call_args
# ask_text("title", label, default_text=..., parent=...)
# default_text wird als Keyword-Arg übergeben
default_text_arg = kwargs.get("default_text", args[2] if len(args) > 2 else "")
self.assertIn("1234", default_text_arg)
self.assertIn("5678", default_text_arg)
# ------------------------------------------------------------------ #
# 7. vorgabe_namen erscheint im Label-Text #
# ------------------------------------------------------------------ #
@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_vorgabe_namen_appears_in_label(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""vorgabe_namen=['Musterstadt'] → ask_text-label enthält den Namen."""
mock_ask_text.return_value = ("9999", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None, vorgabe_namen=["Musterstadt"])
args, kwargs = mock_ask_text.call_args
# label ist das 2. Positional-Argument
label_arg = args[1] if len(args) > 1 else kwargs.get("label", "")
self.assertIn("Musterstadt", label_arg)
# ------------------------------------------------------------------ #
# 8. bbox wird in WFS-URI eingebettet #
# ------------------------------------------------------------------ #
@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.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
):
"""flurstueck_alkis(tab) ohne neue Parameter → kein Fehler."""
mock_ask_text.return_value = ("0000", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
layer, status = flurstueck_alkis(None)
self.assertIsNotNone(layer)
self.assertEqual(status, LoadStatus.FIRST)
# ---------------------------------------------------------------------------
# Dummies für den Anhängen-Deduplizierungstest (#28)
# ---------------------------------------------------------------------------
class _DummyFlstFeature:
def __init__(self, kennzeichen):
self._kennzeichen = kennzeichen
def __getitem__(self, key):
if key == FELD_FLURSTUECKSKENNZEICHEN:
return self._kennzeichen
return None
class _DummyFlstFields:
def __init__(self, hat_kennzeichen_feld: bool):
self._hat_feld = hat_kennzeichen_feld
def indexFromName(self, name):
if name == FELD_FLURSTUECKSKENNZEICHEN and self._hat_feld:
return 0
return -1
class _DummyFlstLayer:
"""Flurstück-Layer-Dummy mit Feature-Liste und aufzeichnendem ``dataProvider``."""
def __init__(self, features, hat_kennzeichen_feld: bool = True):
self._features = list(features)
self._hat_feld = hat_kennzeichen_feld
self.added = None
def getFeatures(self):
return list(self._features)
def fields(self):
return _DummyFlstFields(self._hat_feld)
def dataProvider(self):
layer = self
class _Provider:
def addFeatures(self, feats):
layer.added = list(feats)
layer._features.extend(feats)
return True
return _Provider()
def updateExtents(self):
pass
def triggerRepaint(self):
pass
def isValid(self):
return True
def featureCount(self):
return len(self._features)
class TestFlurstueckAlkisAnhaengenDedup(unittest.TestCase):
"""Anhängen an einen bestehenden Flurstück-Layer (Issue #28)."""
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_append_ueberspringt_bereits_vorhandene_flurstuecke(
self, mock_ask_text, mock_check, mock_lade
):
"""Prüft, dass beim Anhängen bereits vorhandene flurstueckskennzeichen nicht erneut hinzugefügt werden."""
mock_ask_text.return_value = ("0001, 0002", True)
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
neu_geladen = _DummyFlstLayer([
_DummyFlstFeature("110001_0001__00001"), # bereits im Layer vorhanden
_DummyFlstFeature("110001_0002__00007"), # noch nicht vorhanden
])
mock_check.return_value = (bestehend, LoadStatus.APPEND)
mock_lade.return_value = neu_geladen
layer, status = flurstueck_alkis(None)
self.assertEqual(status, LoadStatus.APPEND)
self.assertIs(layer, bestehend)
self.assertEqual(len(bestehend.added), 1)
self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_append_ohne_kennzeichen_feld_haengt_alles_an(
self, mock_ask_text, mock_check, mock_lade
):
"""Prüft, dass ohne flurstueckskennzeichen-Feld weiterhin alle Features angehängt werden (Rückfallverhalten)."""
mock_ask_text.return_value = ("0001", True)
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")], hat_kennzeichen_feld=False)
neu_geladen = _DummyFlstLayer(
[_DummyFlstFeature("110001_0001__00001"), _DummyFlstFeature("110001_0002__00007")],
hat_kennzeichen_feld=False,
)
mock_check.return_value = (bestehend, LoadStatus.APPEND)
mock_lade.return_value = neu_geladen
layer, status = flurstueck_alkis(None)
self.assertEqual(status, LoadStatus.APPEND)
self.assertEqual(len(bestehend.added), 2)
if __name__ == "__main__":
unittest.main()