Files
Plugin_SN_Verfahrensgebiet/tests/test_flurstueck_alkis.py
T

472 lines
20 KiB
Python
Raw Normal View History

2026-04-21 21:50:26 +02:00
"""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,
)
2026-04-21 21:50:26 +02:00
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
2026-04-22 14:53:45 +02:00
# ------------------------------------------------------------------ #
# 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)
# ------------------------------------------------------------------ #
# 10. 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")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
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."""
mock_project.return_value = _DummyProject()
geladen = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
mock_lade.return_value = geladen
layer, status = flurstueck_alkis(
None, vorgabe_schluessel=["1234", "5678"], ohne_schluessel_abfrage=True
)
mock_ask_text.assert_not_called()
self.assertIs(layer, geladen)
self.assertEqual(status, LoadStatus.FIRST)
mock_lade.assert_called_once_with(["1234", "5678"])
2026-04-22 14:53:45 +02:00
# ---------------------------------------------------------------------------
# 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)
@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_ohne_schluessel_abfrage_append_dedup(self, mock_ask_text, mock_check, mock_lade):
"""ohne_schluessel_abfrage=True: Anhängen dedupliziert trotzdem, ohne nach Schlüsseln zu fragen (#47)."""
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
neu_geladen = _DummyFlstLayer([
_DummyFlstFeature("110001_0001__00001"), # bereits vorhanden
_DummyFlstFeature("110001_0002__00007"), # neu
])
mock_check.return_value = (bestehend, LoadStatus.APPEND)
mock_lade.return_value = neu_geladen
layer, status = flurstueck_alkis(
None, vorgabe_schluessel=["0001", "0002"], ohne_schluessel_abfrage=True
)
mock_ask_text.assert_not_called()
self.assertEqual(status, LoadStatus.APPEND)
self.assertEqual(len(bestehend.added), 1)
self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007")
2026-04-21 21:50:26 +02:00
if __name__ == "__main__":
unittest.main()