diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..4721e18 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests für das Plugin sn_verfahrensgebiet.""" diff --git a/tests/test_dockwidget.py b/tests/test_dockwidget.py new file mode 100644 index 0000000..da413e9 --- /dev/null +++ b/tests/test_dockwidget.py @@ -0,0 +1,86 @@ +import sys +import types +import unittest +from unittest.mock import patch + +if "qgis" not in sys.modules: + qgis_mod = types.ModuleType("qgis") + qgis_utils = types.ModuleType("qgis.utils") + qgis_utils.iface = types.SimpleNamespace() + qgis_utils.plugins = {} + qgis_mod.utils = qgis_utils + sys.modules["qgis"] = qgis_mod + sys.modules["qgis.utils"] = qgis_utils + +from sn_verfahrensgebiet.ui.dockwidget import DockWidget + + +class _DummyTabWidget: + def __init__(self): + self.tabs = [] + + def addTab(self, widget, title): + self.tabs.append((widget, title)) + + +class _TabWithServices: + tab_title = "WithServices" + + def __init__(self, _parent): + self.services = None + + def set_services(self, pruefmanager=None, data_grabber=None): + self.services = (pruefmanager, data_grabber) + + +class _TabWithoutServices: + tab_title = "WithoutServices" + + def __init__(self, _parent): + pass + + +class _TabRaises: + tab_title = "Broken" + + def __init__(self, _parent): + raise RuntimeError("boom") + + +class TestDockWidget(unittest.TestCase): + + @patch("sn_verfahrensgebiet.ui.dockwidget.QTabWidget", _DummyTabWidget) + def test_init_tabs_with_services_injects_available_services(self): + obj = DockWidget.__new__(DockWidget) + obj.tabs = [_TabWithServices, _TabWithoutServices] + obj.pruefmanager = object() + obj.data_grabber = object() + obj._set_widget_calls = [] + obj.setWidget = lambda value: obj._set_widget_calls.append(value) + + DockWidget._init_tabs_with_services(obj) + + self.assertEqual(len(obj._set_widget_calls), 2) + tab_widget = obj._set_widget_calls[-1] + self.assertIsInstance(tab_widget, _DummyTabWidget) + self.assertEqual(len(tab_widget.tabs), 2) + + first_tab = tab_widget.tabs[0][0] + self.assertEqual(first_tab.services, (obj.pruefmanager, obj.data_grabber)) + + @patch("sn_verfahrensgebiet.ui.dockwidget.error") + @patch("sn_verfahrensgebiet.ui.dockwidget.QTabWidget", _DummyTabWidget) + def test_init_tabs_with_services_reports_errors(self, mock_error): + obj = DockWidget.__new__(DockWidget) + obj.tabs = [_TabRaises] + obj.pruefmanager = object() + obj.data_grabber = object() + obj.setWidget = lambda _value: None + + DockWidget._init_tabs_with_services(obj) + + mock_error.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..0f029f3 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,121 @@ +import sys +import types +import unittest +from types import SimpleNamespace +from unittest.mock import Mock, patch + +if "qgis" not in sys.modules: + qgis_mod = types.ModuleType("qgis") + qgis_utils = types.ModuleType("qgis.utils") + qgis_utils.iface = types.SimpleNamespace() + qgis_utils.plugins = {} + qgis_mod.utils = qgis_utils + sys.modules["qgis"] = qgis_mod + sys.modules["qgis.utils"] = qgis_utils + +from sn_verfahrensgebiet.main import Verfahrensgebiet + + +class _DummyIface: + def __init__(self): + self.removed = [] + + def mainWindow(self): + return object() + + def removeDockWidget(self, dock): + self.removed.append(dock) + + +class _DummyDock: + def __init__(self): + self.object_name = None + self.action = None + self.deleted = False + + def setObjectName(self, name): + self.object_name = name + + def deleteLater(self): + self.deleted = True + + +class TestPluginMain(unittest.TestCase): + + @patch("sn_verfahrensgebiet.main.DataGrabber") + @patch("sn_verfahrensgebiet.main.Pruefmanager") + def test_init_sets_services_and_names(self, mock_pruefmanager, mock_data_grabber): + pm = object() + dg = object() + mock_pruefmanager.return_value = pm + mock_data_grabber.return_value = dg + + plugin = Verfahrensgebiet(_DummyIface()) + + self.assertIs(plugin.pruefmanager, pm) + self.assertIs(plugin.data_grabber, dg) + self.assertEqual(plugin.plugin_name, "Verfahrensgebiet") + self.assertEqual(plugin.dock_name, "sn_dock_verfahrensgebiet") + + @patch("sn_verfahrensgebiet.main.plugins", new_callable=dict) + def test_initGui_registers_action_when_basis_ui_exists(self, mock_plugins): + iface = _DummyIface() + plugin = Verfahrensgebiet(iface) + + ui = SimpleNamespace( + add_action=Mock(return_value="action_obj"), + finalize_menu_and_toolbar=Mock(), + ) + mock_plugins["sn_basis"] = SimpleNamespace(ui=ui) + + plugin.initGui() + + self.assertEqual(plugin.action, "action_obj") + ui.add_action.assert_called_once() + ui.finalize_menu_and_toolbar.assert_called_once() + + @patch("sn_verfahrensgebiet.main.plugins", new_callable=dict) + @patch("sn_verfahrensgebiet.main.DockManager.show") + @patch("sn_verfahrensgebiet.main.DockWidget") + def test_run_creates_dock_and_marks_toolbar_active(self, mock_dockwidget_cls, mock_show, mock_plugins): + iface = _DummyIface() + plugin = Verfahrensgebiet(iface) + plugin.action = "action_obj" + + dock = _DummyDock() + mock_dockwidget_cls.return_value = dock + + ui = SimpleNamespace(set_active_plugin=Mock()) + mock_plugins["sn_basis"] = SimpleNamespace(ui=ui) + + plugin.run() + + self.assertIs(plugin.dockwidget, dock) + self.assertEqual(dock.object_name, "sn_dock_verfahrensgebiet") + self.assertEqual(dock.action, "action_obj") + mock_show.assert_called_once_with(dock) + ui.set_active_plugin.assert_called_once_with("action_obj") + + @patch("sn_verfahrensgebiet.main.plugins", new_callable=dict) + def test_unload_removes_dock_and_action(self, mock_plugins): + iface = _DummyIface() + plugin = Verfahrensgebiet(iface) + + dock = _DummyDock() + plugin.dockwidget = dock + plugin.action = "action_obj" + + ui = SimpleNamespace(remove_action=Mock()) + mock_plugins["sn_basis"] = SimpleNamespace(ui=ui) + + plugin.unload() + + self.assertIn(dock, iface.removed) + self.assertTrue(dock.deleted) + self.assertIsNone(plugin.dockwidget) + ui.remove_action.assert_called_once_with("action_obj") + self.assertIsNone(plugin.action) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_verfahrensgebiet_alkis.py b/tests/test_verfahrensgebiet_alkis.py new file mode 100644 index 0000000..46ba82b --- /dev/null +++ b/tests/test_verfahrensgebiet_alkis.py @@ -0,0 +1,184 @@ +import sys +import types +import unittest +from unittest.mock import patch + +if "qgis" not in sys.modules: + qgis_mod = types.ModuleType("qgis") + qgis_utils = types.ModuleType("qgis.utils") + qgis_utils.iface = types.SimpleNamespace() + qgis_utils.plugins = {} + qgis_mod.utils = qgis_utils + sys.modules["qgis"] = qgis_mod + sys.modules["qgis.utils"] = qgis_utils + +from sn_verfahrensgebiet.functions import verfahrensgebiet_alkis as vg + + +class _DummyMessageBox: + class StandardButton: + Yes = 1 + No = 2 + + last_critical = None + next_reply = StandardButton.No + + @staticmethod + def question(_parent, _title, _message, _buttons, _default): + return _DummyMessageBox.next_reply + + @staticmethod + def critical(parent, title, message): + _DummyMessageBox.last_critical = (parent, title, message) + + +class _DummyProject: + def __init__(self, layers=None): + self._layers = layers or [] + self.removed = [] + + def mapLayersByName(self, _name): + return self._layers + + def removeMapLayer(self, layer_id): + self.removed.append(layer_id) + + +class _DummyExistingLayer: + def __init__(self, layer_id="existing"): + self._id = layer_id + + def id(self): + return self._id + + +class _DummyGeom: + def __init__(self, empty=False): + self._empty = empty + + def isEmpty(self): + return self._empty + + +class _DummyFeature: + def __init__(self, geom=None): + self._geom = geom or _DummyGeom() + + def geometry(self): + return self._geom + + +class _DummyTempLayer: + def __init__(self, valid=True, features=None): + self._valid = valid + self._features = features or [] + + def isValid(self): + return self._valid + + def getFeatures(self): + return self._features + + def crs(self): + class _Crs: + @staticmethod + def authid(): + return "EPSG:25833" + + return _Crs() + + +class _DummyProvider: + def addAttributes(self, _attrs): + return True + + def addFeatures(self, _features): + return True + + +class _DummyMemoryLayer: + def __init__(self): + self._provider = _DummyProvider() + + def dataProvider(self): + return self._provider + + def updateFields(self): + return None + + def updateExtents(self): + return None + + +class TestVerfahrensgebietAlkis(unittest.TestCase): + + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsProject.instance") + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox) + def test_check_existing_layer_first_load(self, mock_project_instance): + mock_project_instance.return_value = _DummyProject(layers=[]) + + layer, status = vg._check_existing_layer(tab_widget=None) + + self.assertIsNone(layer) + self.assertEqual(status, vg.LoadStatus.FIRST) + + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsProject.instance") + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox) + def test_check_existing_layer_keep_existing(self, mock_project_instance): + existing = _DummyExistingLayer("L1") + mock_project_instance.return_value = _DummyProject(layers=[existing]) + _DummyMessageBox.next_reply = _DummyMessageBox.StandardButton.No + + layer, status = vg._check_existing_layer(tab_widget=None) + + self.assertIs(layer, existing) + self.assertEqual(status, vg.LoadStatus.KEEP) + + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox) + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="") + def test_verfahrensgebiet_alkis_fails_when_verfahrensnummer_missing(self, _mock_get_var): + _DummyMessageBox.last_critical = None + + layer, status = vg.verfahrensgebiet_alkis(tab_widget=None) + + self.assertIsNone(layer) + self.assertEqual(status, vg.LoadStatus.NONE) + self.assertIsNotNone(_DummyMessageBox.last_critical) + + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST)) + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox) + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123") + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer") + def test_verfahrensgebiet_alkis_fails_on_invalid_wfs_layer(self, mock_qgsvectorlayer, _mock_get_var, _mock_check): + _DummyMessageBox.last_critical = None + mock_qgsvectorlayer.return_value = _DummyTempLayer(valid=False) + + layer, status = vg.verfahrensgebiet_alkis(tab_widget=None) + + self.assertIsNone(layer) + self.assertEqual(status, vg.LoadStatus.NONE) + self.assertIsNotNone(_DummyMessageBox.last_critical) + + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsFeature") + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsGeometry.unaryUnion", return_value=_DummyGeom(empty=False)) + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST)) + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123") + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer") + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox) + def test_verfahrensgebiet_alkis_success_returns_memory_layer(self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls): + temp_layer = _DummyTempLayer(valid=True, features=[_DummyFeature()]) + memory_layer = _DummyMemoryLayer() + mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer] + + mock_feature = mock_feature_cls.return_value + mock_feature.setGeometry.return_value = None + mock_feature.setAttributes.return_value = None + + layer, status = vg.verfahrensgebiet_alkis(tab_widget=None) + + self.assertIs(layer, memory_layer) + self.assertEqual(status, vg.LoadStatus.FIRST) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_working_tab.py b/tests/test_working_tab.py new file mode 100644 index 0000000..683dd27 --- /dev/null +++ b/tests/test_working_tab.py @@ -0,0 +1,156 @@ +import sys +import types +import unittest +from unittest.mock import patch + +if "qgis" not in sys.modules: + qgis_mod = types.ModuleType("qgis") + qgis_utils = types.ModuleType("qgis.utils") + qgis_utils.iface = types.SimpleNamespace( + mapCanvas=lambda: types.SimpleNamespace(setExtent=lambda _e: None, refresh=lambda: None) + ) + qgis_utils.plugins = {} + qgis_mod.utils = qgis_utils + sys.modules["qgis"] = qgis_mod + sys.modules["qgis.utils"] = qgis_utils + +from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab, LoadStatus + + +class _DummyLayer: + def __init__(self, valid=True, 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): + self.added = [] + + def addMapLayer(self, layer): + self.added.append(layer) + + +class _DummyCanvas: + def __init__(self): + self.extent = None + + def setExtent(self, extent): + self.extent = extent + + def refresh(self): + return None + + +class _DummyButton: + def __init__(self): + self._enabled = True + self._text = "Aus ALKIS laden" + + def setEnabled(self, enabled): + self._enabled = enabled + + def isEnabled(self): + return self._enabled + + def setText(self, text): + self._text = text + + def text(self): + return self._text + + +class _DummyCombo: + def __init__(self): + self.layer = None + + def setLayer(self, layer): + self.layer = layer + + +class TestWorkingTab(unittest.TestCase): + + def _make_tab(self): + tab = WorkingTab.__new__(WorkingTab) + tab.btn_verf_alkis = _DummyButton() + tab.verf_layer_combo = _DummyCombo() + tab.haken_verf = object() + tab.setze_haken = lambda *_args, **_kwargs: None + return tab + + def test_set_busy_updates_button_state(self): + tab = self._make_tab() + + tab._set_busy(tab.btn_verf_alkis, True) + self.assertFalse(tab.btn_verf_alkis.isEnabled()) + self.assertEqual(tab.btn_verf_alkis.text(), "Lade ...") + + tab._set_busy(tab.btn_verf_alkis, False) + self.assertTrue(tab.btn_verf_alkis.isEnabled()) + self.assertEqual(tab.btn_verf_alkis.text(), "Aus ALKIS laden") + + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id") + def test_on_verf_layer_changed_valid_sets_variable(self, mock_set_layer_id): + tab = self._make_tab() + layer = _DummyLayer(valid=True, layer_id="layer_123") + + tab._on_verf_layer_changed(layer) + + mock_set_layer_id.assert_called_once_with("layer_123") + + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id") + def test_on_verf_layer_changed_none_clears_variable(self, mock_set_layer_id): + tab = self._make_tab() + + tab._on_verf_layer_changed(None) + + mock_set_layer_id.assert_called_once_with("") + + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.success") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id") + def test_handle_verf_alkis_success_path( + self, + mock_set_layer_id, + mock_iface, + mock_alkis, + mock_project_instance, + mock_apply_style, + mock_success, + ): + tab = self._make_tab() + layer = _DummyLayer(valid=True, layer_id="layer_ok") + mock_alkis.return_value = (layer, LoadStatus.FIRST) + mock_project_instance.return_value = _DummyProject() + mock_iface.mapCanvas.return_value = _DummyCanvas() + + tab.handle_verf_alkis() + + mock_apply_style.assert_called_once() + mock_success.assert_called_once() + mock_set_layer_id.assert_called_with("layer_ok") + + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id") + @patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis", return_value=(None, LoadStatus.NONE)) + def test_handle_verf_alkis_failure_clears_layer_var(self, _mock_alkis, mock_set_layer_id): + tab = self._make_tab() + + tab.handle_verf_alkis() + + mock_set_layer_id.assert_called_once_with("") + + +if __name__ == "__main__": + unittest.main()