auf Wrapper umgestellt, tests ergänzt

This commit is contained in:
2026-01-08 17:13:43 +01:00
parent 8f8a1ccde3
commit 3bfd88b51e
13 changed files with 826 additions and 473 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
#Testordner

148
tests/run_tests.py Normal file
View File

@@ -0,0 +1,148 @@
"""
sn_plan41/test/run_tests.py
Zentraler Test-Runner für sn_plan41.
Wrapper-konform, QGIS-unabhängig, CI- und IDE-fähig.
"""
import unittest
import datetime
import inspect
import os
import sys
from pathlib import Path
# ---------------------------------------------------------
# Plugin-Roots bestimmen
# ---------------------------------------------------------
THIS_FILE = Path(__file__).resolve()
# .../plugins/sn_plan41
SN_PLAN41_ROOT = THIS_FILE.parents[1]
# .../plugins/sn_basis
SN_BASIS_ROOT = SN_PLAN41_ROOT.parent / "sn_basis"
# ---------------------------------------------------------
# sys.path Bootstrap
# ---------------------------------------------------------
for path in (SN_PLAN41_ROOT, SN_BASIS_ROOT):
path_str = str(path)
if path_str not in sys.path:
sys.path.insert(0, path_str)
# ---------------------------------------------------------
# Farben
# ---------------------------------------------------------
RED = "\033[91m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
CYAN = "\033[96m"
MAGENTA = "\033[95m"
RESET = "\033[0m"
GLOBAL_TEST_COUNTER = 0
# ---------------------------------------------------------
# Farbige TestResult-Klasse
# ---------------------------------------------------------
class ColoredTestResult(unittest.TextTestResult):
_last_test_class = None
def startTest(self, test):
global GLOBAL_TEST_COUNTER
GLOBAL_TEST_COUNTER += 1
self.stream.write(f"{CYAN}[Test {GLOBAL_TEST_COUNTER}]{RESET}\n")
super().startTest(test)
def startTestClass(self, test):
cls = test.__class__
file = inspect.getfile(cls)
filename = os.path.basename(file)
self.stream.write(
f"\n{MAGENTA}{'=' * 70}\n"
f"Starte Testklasse: {filename}{cls.__name__}\n"
f"{'=' * 70}{RESET}\n"
)
def addError(self, test, err):
super().addError(test, err)
self.stream.write(f"{RED}ERROR{RESET}\n")
def addFailure(self, test, err):
super().addFailure(test, err)
self.stream.write(f"{RED}FAILURE{RESET}\n")
def addSkip(self, test, reason):
super().addSkip(test, reason)
self.stream.write(f"{YELLOW}SKIPPED{RESET}: {reason}\n")
def addSuccess(self, test):
super().addSuccess(test)
self.stream.write(f"{GREEN}OK{RESET}\n")
# ---------------------------------------------------------
# Farbiger TestRunner
# ---------------------------------------------------------
class ColoredTestRunner(unittest.TextTestRunner):
def _makeResult(self):
result = ColoredTestResult(
self.stream,
self.descriptions,
self.verbosity,
)
original_start_test = result.startTest
def patched_start_test(test):
if result._last_test_class != test.__class__:
result.startTestClass(test)
result._last_test_class = test.__class__
original_start_test(test)
result.startTest = patched_start_test
return result
# ---------------------------------------------------------
# Testlauf starten
# ---------------------------------------------------------
def main():
print("\n" + "=" * 70)
print(
f"{CYAN}Testlauf gestartet am: "
f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}"
)
print("=" * 70 + "\n")
loader = unittest.TestLoader()
TEST_ROOT = SN_PLAN41_ROOT / "tests"
suite = loader.discover(
start_dir=str(TEST_ROOT),
pattern="test_*.py",
top_level_dir=str(SN_PLAN41_ROOT.parent),
)
runner = ColoredTestRunner(verbosity=2)
result = runner.run(suite)
return 0 if result.wasSuccessful() else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,9 @@
@echo off
SET OSGEO4W_ROOT=D:\QGISQT5
call %OSGEO4W_ROOT%\bin\o4w_env.bat
set QGIS_PREFIX_PATH=%OSGEO4W_ROOT%\apps\qgis
set PYTHONPATH=%QGIS_PREFIX_PATH%\python;%PYTHONPATH%
set PATH=%OSGEO4W_ROOT%\bin;%QGIS_PREFIX_PATH%\bin;%PATH%
REM Neue Eingabeaufforderung starten und Python-Skript ausführen
start cmd /k "python run_tests.py"

153
tests/test_tab_a_logic.py Normal file
View File

@@ -0,0 +1,153 @@
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore
from sn_basis.functions.variable_wrapper import get_variable # type: ignore
from sn_basis.functions.sys_wrapper import file_exists # type: ignore
class TestTabALogic(unittest.TestCase):
# -----------------------------------------------------
# 1. Verfahrens-DB setzen und laden
# -----------------------------------------------------
def test_verfahrens_db_set_and_load(self):
logic = TabALogic()
with TemporaryDirectory() as tmp:
db_path = Path(tmp) / "test.gpkg"
db_path.write_text("")
logic.set_verfahrens_db(str(db_path))
stored = get_variable("verfahrens_db", scope="project")
self.assertEqual(stored, str(db_path))
loaded = logic.load_verfahrens_db()
self.assertEqual(loaded, str(db_path))
# -----------------------------------------------------
# 2. Verfahrens-DB löschen
# -----------------------------------------------------
def test_verfahrens_db_clear(self):
logic = TabALogic()
logic.set_verfahrens_db(None)
stored = get_variable("verfahrens_db", scope="project")
self.assertEqual(stored, "")
# -----------------------------------------------------
# 3. Neue Verfahrens-DB anlegen
# -----------------------------------------------------
def test_create_new_verfahrens_db(self):
logic = TabALogic()
with TemporaryDirectory() as tmp:
db_path = Path(tmp) / "neu.gpkg"
result = logic.create_new_verfahrens_db(str(db_path))
self.assertTrue(result)
self.assertTrue(file_exists(db_path))
stored = get_variable("verfahrens_db", scope="project")
self.assertEqual(stored, str(db_path))
def test_create_new_verfahrens_db_with_none_path(self):
logic = TabALogic()
result = logic.create_new_verfahrens_db(None)
self.assertFalse(result)
# -----------------------------------------------------
# 4. Linkliste setzen und laden
# -----------------------------------------------------
def test_linkliste_set_and_load(self):
logic = TabALogic()
with TemporaryDirectory() as tmp:
link_path = Path(tmp) / "links.xlsx"
link_path.write_text("dummy")
logic.set_linkliste(str(link_path))
stored = get_variable("linkliste", scope="project")
self.assertEqual(stored, str(link_path))
loaded = logic.load_linkliste()
self.assertEqual(loaded, str(link_path))
# -----------------------------------------------------
# 5. Linkliste löschen
# -----------------------------------------------------
def test_linkliste_clear(self):
logic = TabALogic()
logic.set_linkliste(None)
stored = get_variable("linkliste", scope="project")
self.assertEqual(stored, "")
# -----------------------------------------------------
# 6. Layer-ID speichern
# -----------------------------------------------------
def test_verfahrensgebiet_layer_id_storage(self):
logic = TabALogic()
class MockLayer:
def id(self):
return "layer-123"
logic.save_verfahrensgebiet_layer(MockLayer())
stored = get_variable("verfahrensgebiet_layer", scope="project")
self.assertEqual(stored, "layer-123")
# -----------------------------------------------------
# 7. Ungültiger Layer wird ignoriert
# -----------------------------------------------------
def test_invalid_layer_is_rejected(self):
logic = TabALogic()
class InvalidLayer:
pass
logic.save_verfahrensgebiet_layer(InvalidLayer())
stored = get_variable("verfahrensgebiet_layer", scope="project")
self.assertEqual(stored, "")
#-----------------------------------------------------
# 8. Layer-ID wirft Exception
#----------------------------------------------------
def test_layer_id_raises_exception(self):
logic = TabALogic()
class BadLayer:
def id(self):
raise RuntimeError("boom")
logic.save_verfahrensgebiet_layer(BadLayer())
stored = get_variable("verfahrensgebiet_layer", scope="project")
self.assertEqual(stored, "")
# -----------------------------------------------------
# 11. Layer ID wird leer zurückgegeben
# -----------------------------------------------------
def test_layer_id_returns_empty(self):
logic = TabALogic()
class EmptyLayer:
def id(self):
return ""
logic.save_verfahrensgebiet_layer(EmptyLayer())
stored = get_variable("verfahrensgebiet_layer", scope="project")
self.assertEqual(stored, "")
if __name__ == "__main__":
unittest.main()

57
tests/test_tab_a_ui.py Normal file
View File

@@ -0,0 +1,57 @@
"""
Smoke-Tests für TabA UI (sn_plan41/ui/tab_a_ui.py)
Ziel:
- UI kann erstellt werden
- Callbacks crashen nicht
- Keine Qt-Verhaltensprüfung
"""
import unittest
from sn_plan41.ui.tab_a_ui import TabA #type:ignore
class TestTabAUI(unittest.TestCase):
# -----------------------------------------------------
# 1. UI kann erstellt werden
# -----------------------------------------------------
def test_tab_a_ui_can_be_created(self):
tab = TabA(parent=None,build_ui=False)
self.assertIsNotNone(tab)
self.assertEqual(tab.tab_title, "Daten")
# -----------------------------------------------------
# 2. Toggle-Callbacks crashen nicht
# -----------------------------------------------------
def test_tab_a_toggle_callbacks_do_not_crash(self):
tab = TabA(parent=None,build_ui=False)
tab._toggle_group(True)
tab._toggle_group(False)
tab._toggle_optional(True)
tab._toggle_optional(False)
# -----------------------------------------------------
# 3. Datei-Callbacks akzeptieren leere Eingaben
# -----------------------------------------------------
def test_tab_a_file_callbacks_accept_empty_input(self):
tab = TabA(parent=None,build_ui=False)
tab._on_verfahrens_db_changed("")
tab._on_linkliste_changed("")
# -----------------------------------------------------
# 4. Layer-Callback akzeptiert None
# -----------------------------------------------------
def test_tab_a_layer_callback_accepts_none(self):
tab = TabA(parent=None,build_ui=False)
tab._on_layer_changed(None)
if __name__ == "__main__":
unittest.main()