it-praxisportal/14_gui_mit_tkinter/test_aufgabe.py

274 lines
No EOL
10 KiB
Python

#!/usr/bin/env python3
"""
🧪 Testsuite für Kapitel 14: Desktop-GUIs mit Tkinter & OOP
============================================================
Überprüft:
- ZaehlerLogik: Reines Datenmodell (Werte, Grenzen, Schrittweiten, Klicks)
- ZaehlerApp: Tkinter-Oberfläche, Widget-Erstellung, Event-Handling, MVC-Synchronisation
- Headless-Kompatibilität: Läuft ohne GUI-Blockade in jeder Testumgebung!
"""
import sys
import unittest
from pathlib import Path
# Pfad zum aktuellen Kapitel hinzufügen
sys.path.insert(0, str(Path(__file__).parent))
try:
import aufgabe
except ImportError as e:
raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}")
class TestKapitel14Logik(unittest.TestCase):
"""Prüft das reine Datenmodell ZaehlerLogik."""
def setUp(self):
ZaehlerLogik = getattr(aufgabe, "ZaehlerLogik", None)
self.assertIsNotNone(
ZaehlerLogik, "Klasse 'ZaehlerLogik' in aufgabe.py nicht gefunden!"
)
self.ZaehlerLogik = ZaehlerLogik
def test_01_logik_initialisierung(self):
"""Prüft Default-Werte und benutzerdefinierte Parameter."""
# Standard-Instanziierung
z1 = self.ZaehlerLogik()
self.assertEqual(z1.wert, 0, "Default-Startwert sollte 0 sein.")
self.assertEqual(z1.min_wert, -100, "Default min_wert sollte -100 sein.")
self.assertEqual(z1.max_wert, 100, "Default max_wert sollte 100 sein.")
self.assertEqual(z1.schrittweite, 1, "Default-Schrittweite sollte 1 sein.")
self.assertEqual(z1.klick_anzahl, 0, "Default-Klickanzahl sollte 0 sein.")
# Custom Instanziierung
z2 = self.ZaehlerLogik(startwert=10, min_wert=-20, max_wert=50)
self.assertEqual(z2.wert, 10)
self.assertEqual(z2.min_wert, -20)
self.assertEqual(z2.max_wert, 50)
def test_02_logik_erhoehen_und_verringern(self):
"""Prüft das Erhöhen und Verringern des Zählerstands und Zählen der Klicks."""
z = self.ZaehlerLogik(startwert=0)
# 1. Erhöhen
res = z.erhoehen()
self.assertEqual(res, 1, "erhoehen() sollte den neuen Wert (1) zurückgeben.")
self.assertEqual(z.wert, 1)
self.assertEqual(z.klick_anzahl, 1)
z.erhoehen()
self.assertEqual(z.wert, 2)
self.assertEqual(z.klick_anzahl, 2)
# 2. Verringern
res_v = z.verringern()
self.assertEqual(res_v, 1, "verringern() sollte den neuen Wert (1) zurückgeben.")
self.assertEqual(z.wert, 1)
self.assertEqual(z.klick_anzahl, 3)
z.verringern()
z.verringern()
self.assertEqual(z.wert, -1)
self.assertEqual(z.klick_anzahl, 5)
def test_03_logik_grenzwerte_clamping(self):
"""Prüft, dass der Wert min_wert und max_wert nicht überschreitet."""
z = self.ZaehlerLogik(startwert=9, min_wert=-5, max_wert=10)
# Erhöhen bis zum Limit
z.erhoehen() # wert = 10
self.assertEqual(z.wert, 10)
# Weiteres Erhöhen darf 10 nicht überschreiten
z.erhoehen()
self.assertEqual(z.wert, 10, "Zählerstand darf max_wert nicht überschreiten!")
# Verringern bis zum Minimal-Limit
z2 = self.ZaehlerLogik(startwert=-4, min_wert=-5, max_wert=10)
z2.verringern() # wert = -5
self.assertEqual(z2.wert, -5)
z2.verringern() # bleibt bei -5
self.assertEqual(z2.wert, -5, "Zählerstand darf min_wert nicht unterschreiten!")
def test_04_logik_schrittweite(self):
"""Prüft das Setzen und Anwenden der Schrittweite."""
z = self.ZaehlerLogik(startwert=0)
# Gültige Schrittweite
ok = z.setze_schrittweite(5)
self.assertTrue(ok, "setze_schrittweite(5) sollte True zurückgeben.")
self.assertEqual(z.schrittweite, 5)
z.erhoehen()
self.assertEqual(z.wert, 5, "erhoehen() sollte bei Schrittweite 5 um 5 steigen.")
z.verringern()
self.assertEqual(z.wert, 0)
# Ungültige Schrittweiten (0 oder negativ)
ok_null = z.setze_schrittweite(0)
self.assertFalse(ok_null, "setze_schrittweite(0) sollte False zurückgeben.")
self.assertEqual(z.schrittweite, 5, "Schrittweite darf sich bei 0 nicht ändern.")
ok_neg = z.setze_schrittweite(-3)
self.assertFalse(ok_neg, "setze_schrittweite(-3) sollte False zurückgeben.")
self.assertEqual(z.schrittweite, 5, "Schrittweite darf sich bei negativen Werten nicht ändern.")
def test_05_logik_zuruecksetzen(self):
"""Prüft das Zurücksetzen auf 0."""
z = self.ZaehlerLogik(startwert=42)
res = z.zuruecksetzen()
self.assertEqual(res, 0, "zuruecksetzen() sollte 0 zurückgeben.")
self.assertEqual(z.wert, 0)
self.assertEqual(z.klick_anzahl, 1)
def test_06_logik_ist_gerade_und_str(self):
"""Prüft die Geradheits-Prüfung und String-Repräsentation."""
z = self.ZaehlerLogik(startwert=0)
self.assertTrue(z.ist_gerade(), "0 ist gerade.")
z.erhoehen() # 1
self.assertFalse(z.ist_gerade(), "1 ist ungerade.")
z.erhoehen() # 2
self.assertTrue(z.ist_gerade(), "2 ist gerade.")
# __str__ Prüfung
text = str(z)
self.assertIn("2", text, "__str__ sollte den aktuellen Zählerstand enthalten.")
class TestKapitel14GUI(unittest.TestCase):
"""Prüft die Tkinter-Benutzeroberfläche ZaehlerApp im Headless-Modus."""
def setUp(self):
ZaehlerApp = getattr(aufgabe, "ZaehlerApp", None)
self.assertIsNotNone(
ZaehlerApp, "Klasse 'ZaehlerApp' in aufgabe.py nicht gefunden!"
)
self.ZaehlerApp = ZaehlerApp
# Hauptfenster (oder MockTk) erzeugen
tk_mod = getattr(aufgabe, "tk", None)
self.assertIsNotNone(tk_mod, "Tkinter-Modul 'tk' in aufgabe.py nicht gefunden!")
self.root = tk_mod.Tk()
self.app = self.ZaehlerApp(self.root)
def _get_widget_text(self, widget) -> str:
"""Hilfsfunktion zum Auslesen des Texts eines Widgets."""
if widget is None:
return ""
if hasattr(widget, "cget"):
try:
return str(widget.cget("text"))
except Exception:
pass
if hasattr(widget, "_text"):
return str(widget._text)
if isinstance(widget, dict):
return str(widget.get("text", ""))
return ""
def test_07_gui_initialisierung(self):
"""Prüft, ob alle Widgets angelegt wurden und die Startwerte stimmen."""
self.assertIsNotNone(getattr(self.app, "root", None), "app.root fehlt!")
self.assertIsNotNone(getattr(self.app, "logik", None), "app.logik fehlt!")
# Widgets überprüfen
self.assertTrue(
hasattr(self.app, "label_anzeige"), "app.label_anzeige fehlt!"
)
self.assertTrue(hasattr(self.app, "label_info"), "app.label_info fehlt!")
self.assertTrue(hasattr(self.app, "btn_plus"), "app.btn_plus fehlt!")
self.assertTrue(hasattr(self.app, "btn_minus"), "app.btn_minus fehlt!")
self.assertTrue(hasattr(self.app, "btn_reset"), "app.btn_reset fehlt!")
self.assertTrue(hasattr(self.app, "entry_schritt"), "app.entry_schritt fehlt!")
# Initialer Text im Anzeige-Label sollte "0" sein
anzeige_text = self._get_widget_text(self.app.label_anzeige)
self.assertEqual(
anzeige_text, "0", f"Startwert im Anzeige-Label sollte '0' sein, war aber '{anzeige_text}'"
)
def test_08_gui_klick_plus_und_minus(self):
"""Prüft, ob klick_plus und klick_minus die Logik und das Label aktualisieren."""
# 1. Plus-Klick auslösen
self.app.klick_plus()
self.assertEqual(self.app.logik.wert, 1, "Logik-Wert sollte 1 sein.")
self.assertEqual(
self._get_widget_text(self.app.label_anzeige),
"1",
"Anzeige-Label sollte nach Plus-Klick '1' zeigen.",
)
# 2. Weiterer Plus-Klick
self.app.klick_plus()
self.assertEqual(self.app.logik.wert, 2)
self.assertEqual(self._get_widget_text(self.app.label_anzeige), "2")
# 3. Minus-Klick
self.app.klick_minus()
self.assertEqual(self.app.logik.wert, 1)
self.assertEqual(self._get_widget_text(self.app.label_anzeige), "1")
def test_09_gui_klick_reset(self):
"""Prüft, ob klick_reset den Zähler und das Label zurücksetzt."""
self.app.klick_plus()
self.app.klick_plus()
self.assertEqual(self.app.logik.wert, 2)
self.app.klick_reset()
self.assertEqual(self.app.logik.wert, 0, "Logik-Wert sollte 0 nach Reset sein.")
self.assertEqual(
self._get_widget_text(self.app.label_anzeige),
"0",
"Anzeige-Label sollte nach Reset '0' zeigen.",
)
def test_10_gui_schrittweite_setzen(self):
"""Prüft, ob klick_schritt_setzen den Wert aus entry_schritt übernimmt."""
# Wert im Entry-Feld anpassen
self.app.entry_schritt.delete(0, "end")
self.app.entry_schritt.insert(0, "10")
# Setzen auslösen
self.app.klick_schritt_setzen()
self.assertEqual(
self.app.logik.schrittweite,
10,
"Schrittweite in Logik sollte auf 10 gesetzt werden.",
)
# Plus klicken -> Sprung um 10
self.app.klick_plus()
self.assertEqual(self.app.logik.wert, 10)
self.assertEqual(self._get_widget_text(self.app.label_anzeige), "10")
def test_11_gui_ungueltige_eingabe_abfangen(self):
"""Prüft robuste Fehlerbehandlung bei ungültigen Entry-Eingaben."""
# Text statt Zahl eingeben
self.app.entry_schritt.delete(0, "end")
self.app.entry_schritt.insert(0, "keine_zahl")
# Darf keinen Absturz/Exception erzeugen
try:
self.app.klick_schritt_setzen()
except Exception as e:
self.fail(f"klick_schritt_setzen() ist bei ungültiger Eingabe abgestürzt: {e}")
# Schrittweite sollte unverändert geblieben sein
self.assertEqual(self.app.logik.schrittweite, 1)
if __name__ == "__main__":
res = unittest.main(verbosity=2, exit=False)
try:
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from gamification import report_single_chapter_result
report_single_chapter_result(Path(__file__).parent.name, res.result.wasSuccessful(), res.result.testsRun)
except Exception:
pass
sys.exit(0 if res.result.wasSuccessful() else 1)