Du übernimmst eine Qt-Desktop-App ohne verlässliche Tests, und der erste Vorschlag lautet: „Wir automatisieren die Oberfläche.“ Das klingt sicher, weil Klicks sichtbar sind. Für die meisten geerbten Codebasen ist es trotzdem der falsche Standard: Ein GUI-Test sagt oft zu spät, was kaputtging, und bricht schon bei harmlosen Änderungen. Ich würde zuerst das Verhalten an den Grenzen zwischen Oberfläche und Anwendung festhalten.
Ein Klicktest ist für geerbte Qt-Apps meist der falsche erste Test
Der naheliegende Einstieg ist eine Aufnahme des bisherigen Bedienwegs: Fenster öffnen, Dialog ausfüllen, Schaltfläche drücken, Ergebnis prüfen. Damit testest du zwar einen echten Nutzerpfad, doch du verknüpfst fachliches Verhalten zugleich mit Fenstergeometrie, Fokus, Timing und Betriebssystemdarstellung. Ändert jemand nur den Text einer Schaltfläche oder die Reihenfolge eines Dialogs, meldet der Test einen Fehler, obwohl die eigentliche Funktion weiterhin stimmt. In einer Codebasis ohne Tests entsteht so schnell Arbeit an der Testsuite, bevor sie einen einzigen unbekannten Defekt eingegrenzt hat.
Ich würde nicht mit einer großen Sammlung von Screenshot-Vergleichen beginnen, weil sie bei Schriftarten, Skalierung und Rendering oft Unterschiede zeigen, die nichts über Datenverlust oder falsche Zustandsübergänge aussagen. Ein Screenshot ist sinnvoll, wenn das Erscheinungsbild selbst die Anforderung ist; für einen Speicherbefehl ist er ein schwacher Beleg. Auch „der Dialog ging zu“ beweist noch nicht, dass die Datei geschrieben, ein Fehler behandelt oder ein ausstehender Vorgang abgebrochen wurde.
Der erste QA-Schritt ist deshalb keine neue Testtechnik, sondern eine Bestandsaufnahme konkreter Folgen. Wähle einen Vorgang, bei dem ein Defekt teuer wäre, etwa das Überschreiben einer vorhandenen Datei. Notiere den Ausgangszustand, die Aktion und das beobachtbare Ergebnis: Welche Datei ändert sich? Welche Fehlermeldung erscheint bei fehlender Berechtigung? Bleibt ein ungespeicherter Zustand erhalten? Diese Fragen liefern Prüfpunkte, die auch dann gelten, wenn das Team später von Qt Widgets auf Qt Quick wechselt.
Den Beitrag Qt Entwicklung Tipps fuer moderne Desktop Apps würde ich deshalb als Entwicklungslektüre behandeln, nicht als Reihenfolge für den Testaufbau: Eine modernisierte Oberfläche senkt das Risiko einer ungeprüften Speicheroperation nicht, solange niemand deren Ergebnis prüft. Lege für jeden ausgewählten Vorgang fest, welche Beobachtung einen echten Fehler von einer bloßen UI-Änderung trennt. Erst danach lohnt sich die Frage, auf welcher Ebene du ihn automatisierst.
Die erste belastbare Naht liegt am Zustandswechsel, nicht am Fenster
In einer alten Qt-Anwendung steckt Geschäftslogik häufig direkt in Slots: Ein Klick löst Validierung, Dateizugriff und Meldungsdialog in derselben Methode aus. Das ist unbequem zu testen, aber kein Grund für einen sofortigen Architekturumbau. Suche zuerst nach einem beobachtbaren Übergang, etwa einem Signal für einen abgeschlossenen Import oder einer Methode, die nach einer Aktion einen Status zurückgibt. Qt Test und QSignalSpy können solche Übergänge prüfen, ohne Mauskoordinaten und Fensterpositionen festzuschreiben.
Das folgende kleine Beispiel läuft mit PySide6 und demonstriert die Mechanik. Speichere es als signaltest.py und starte es mit python signaltest.py; PySide6 muss installiert sein. Der künstliche Probe-Typ ersetzt ausdrücklich keinen Test der geerbten Anwendung. Er zeigt lediglich, wie ein erwartetes Signal samt Nutzwert geprüft wird.
from PySide6.QtCore import QCoreApplication, QObject, Signal, QTimer
from PySide6.QtTest import QSignalSpy
class Probe(QObject):
ready = Signal(int)
app = QCoreApplication([])
probe = Probe()
spy = QSignalSpy(probe.ready)
QTimer.singleShot(0, lambda: probe.ready.emit(42))
assert spy.wait(200), "Signal blieb aus"
assert spy.count() == 1
assert spy.at(0)[0] == 42
print("Signalvertrag erfüllt")
Die 200 Millisekunden sind hier ein einstellbarer Demonstrations-Timeout, kein Leistungsversprechen für CI. Entscheidend ist die Aussage des Tests: Das Signal kam einmal und transportierte den erwarteten Wert. Ersetze im nächsten Schritt Probe durch ein vorhandenes Objekt der Anwendung und löse eine reale Aktion aus. Prüfe neben dem Erfolg auch den Fehlerpfad; ein Import, der bei einer beschädigten Datei stillschweigend „fertig“ meldet, besteht sonst denselben oberflächlichen Test.
Ist kein sinnvoller Übergang zugänglich, setze eine kleine Naht vor eine externe Abhängigkeit: etwa eine austauschbare Schnittstelle für Dateizugriffe, statt das gesamte Fenster neu zu schreiben. Ein temporäres Verzeichnis schafft reproduzierbare Eingaben; ein Test mit absichtlich nicht lesbarer Datei prüft die Fehlerbehandlung. QTemporaryDir und QSaveFile sind dabei konkretere Ansatzpunkte als ein Mock für jeden Methodenaufruf. QSaveFile verdient besondere Aufmerksamkeit, weil sein Commit-Schritt darüber entscheidet, ob ein neuer Dateistand übernommen wird.
Halte auch die Grenze dieses Ansatzes fest: Ein bestandener QSignalSpy-Test beweist nicht, dass der Nutzer den Befehl im Menü erreichen kann. Er beweist einen engeren Vertrag und liefert gerade deshalb eine präzisere Fehlermeldung. Für die Erreichbarkeit bleibt später ein kleiner Test über die Oberfläche nötig.
QSignalSpy gewinnt bei Ursachenfragen, Squish bei Bedienpfaden
Zwischen einem signalnahen Test mit Qt Test und QSignalSpy und einem UI-Test mit Squish liegt keine Rangfolge der „professionelleren“ Werkzeuge. Beide beantworten andere Fragen. QSignalSpy gewinnt, wenn ein QA-Team herausfinden muss, ob nach einer Aktion der richtige Zustand entsteht: Der Test läuft ohne vollständigen Desktop, und ein Fehlschlag verweist meist auf einen überschaubaren Teil des Codes. Sein Preis ist eine Testnaht im Anwendungscode; außerdem übersieht er einen Menüeintrag, der versehentlich entfernt wurde.
Squish gewinnt, wenn der tatsächlich bedienbare Pfad zählt, etwa ob ein Nutzer eine Aktion finden, einen Dialog bestätigen und anschließend eine Meldung sehen kann. Sein Preis sind Pflegeaufwand für Objektidentifikation und Testumgebung sowie eine Lizenzentscheidung. Ein fehlgeschlagener UI-Lauf braucht außerdem häufig zusätzliche Diagnose, weil dieselbe rote Meldung durch Fokus, Timing oder einen fachlichen Defekt ausgelöst werden kann. Für eine geerbte App würde ich deshalb wenige kritische Bedienpfade mit Squish absichern und die Varianten darunter mit Qt Test prüfen, statt jeden Eingabefehler durch dieselben Dialoge zu klicken.
Den Beitrag Qt Programmierung Tipps fuer robuste Desktop Apps würde ich an dieser Stelle nicht als Argument für möglichst viele Tests an der Oberfläche verwenden, weil Robustheit ohne erkennbare Fehlerursache nur schwer wiederherzustellen ist. Wenn ein Import mit gültiger, leerer und beschädigter Datei geprüft werden muss, liefern drei zustandsnahe Tests meist eine klarere Diagnose als drei nahezu identische aufgezeichnete Bedienfolgen.
Es gibt eine wichtige Ausnahme: Wenn ein Defekt nur durch das Zusammenspiel von Fenster, Plattform-Plugin und Eingabegerät entsteht, kann ein signalnaher Test ihn nicht reproduzieren. Dann ist ein echter UI-Lauf angemessen. Dokumentiere dabei, ob der Test unter X11, Wayland oder dem Qt-Plugin offscreen läuft; QT_QPA_PLATFORM=offscreen spart einen sichtbaren Desktop, bildet aber nicht jede Interaktion eines produktiven Fensters ab. Diese Angabe verhindert, dass ein grüner CI-Lauf mehr verspricht, als er tatsächlich geprüft hat.
Ohne feste Fehlersignale wird auch die neue Testsuite wieder unzuverlässig
Ein geerbtes Projekt hat meist bereits eine Build- und Release-Routine. Hänge die ersten Tests dort ein, statt sie nur lokal in einer IDE auszuführen. Bei CMake und CTest bedeutet das, Tests im Build zu registrieren und im CI-Lauf ctest –output-on-failure zu starten. Fixiere die verwendete Qt-Version im Build-Protokoll; Qt 6.8 ist hier eine beispielhafte Projektversion, keine Aussage darüber, welche Version du übernehmen solltest. Ohne Versionsangabe lassen sich Unterschiede zwischen lokalem Rechner und Runner unnötig schwer einordnen.
Miss vor jeder Verschärfung der Pipeline, wie sich die vorhandenen Tests verhalten. Führe denselben Lauf mehrfach auf demselben Commit aus und notiere Testname, Laufzeit und Art des Fehlschlags. 30 Wiederholungen sind dafür eine bewusst zu wählende Stichprobe, keine Garantie für Fehlerfreiheit: Wenn ein Test darin einmal scheitert, solltest du ihn untersuchen, bevor sein Ergebnis Releases blockiert. Erhöhe Timeouts nicht reflexhaft. Ein Timeout kann einen langsamen Runner abfedern; er kann ebenso einen verlorenen Callback verdecken, weshalb Logs und Zustandsprüfungen zur Diagnose gehören.
Ergänze danach Prüfungen, die GUI-Tests nicht ersetzen können. AddressSanitizer findet bestimmte Speicherfehler in nativem C++-Code; ThreadSanitizer hilft bei Datenrennen, verursacht aber zusätzliche Laufzeit und sollte in einer passenden separaten Build-Konfiguration laufen. clang-tidy liefert statische Hinweise, doch ein Hinweis ist noch kein reproduzierter Defekt. gcov und lcov zeigen, welcher Code ausgeführt wurde; eine hohe Abdeckung beweist keine korrekten Assertions. Diese Werkzeuge sind besonders nützlich, wenn du ihre Funde auf einen zuvor festgelegten Risikopfad beziehst, statt eine möglichst große Kennzahl zum Selbstzweck zu machen.
Bei asynchronem Code solltest du außerdem die Ereignisschleife ausdrücklich berücksichtigen. QTimer, Queued Connections und Worker-Threads liefern Ergebnisse nicht zwingend in der Reihenfolge, in der ein Test seine Zeilen ausführt. Ein willkürliches Sleep macht solche Tests langsam und bleibt auf anderen Rechnern unsicher; QSignalSpy oder eine gezielte Bedingung beschreibt dagegen, worauf der Test tatsächlich wartet. Protokolliere bei jedem Fehlschlag auch die Eingabedatei und den letzten bekannten Zustand, damit aus „CI rot“ eine bearbeitbare Beobachtung wird.
Der erste neue Test sollte einen bekannten Schaden verhindern
Nimm morgen einen einzigen Vorgang, bei dem die Anwendung Daten verlieren oder falsch überschreiben könnte. Reproduziere seinen aktuellen Ablauf mit einer festen Eingabe und schreibe zuerst den erwarteten Dateizustand auf. Baue dann den kleinsten Test, der genau diesen Zustand prüft, und lasse ihn im bestehenden CI laufen. Einen UI-Test ergänzt du erst, wenn geklärt ist, welche Lücke zwischen geprüfter Funktion und tatsächlicher Bedienung offenbleibt.



