Eine geerbte WinForms-Anwendung ohne Tests lädt zu einer großen Aufräumaktion ein: Formulare entflechten, Framework aktualisieren, Oberfläche automatisieren. Ich würde anders beginnen. Als QA-Ingenieur brauchst du zuerst eine überprüfbare Grenze um das bestehende Verhalten, selbst wenn dieses Verhalten unschön ist. Meine Position: Eine kleine, gezielt getestete Änderung an einem häufig genutzten Ablauf senkt das Änderungsrisiko stärker als eine frühe Modernisierung der gesamten Anwendung.
Der erste Eingriff gehört an die Beobachtungsgrenze, nicht ins Formular-Design
Bevor du Code verschiebst, halte einen vollständigen Ablauf fest: Anwendung starten, Datensatz öffnen, einen Wert ändern, speichern und erneut öffnen. Notiere Eingaben, sichtbare Meldungen, Datenbankänderungen und den Zeitpunkt, an dem die Oberfläche nicht mehr reagiert. Ein Screenshot allein reicht dafür nicht, weil er weder einen fehlgeschlagenen Schreibvorgang noch einen Fehler beim erneuten Laden belegt. Speichere die Beobachtung zusammen mit Commit-ID, Windows-Version, Benutzerrechten und verwendeter Datenbank in Git; sonst lässt sich ein späterer Unterschied kaum einer Codeänderung zuordnen.
Die Einführung in .NET WinForms Grundlagen: Moderne Desktop Apps entwickeln kann das Formularmodell verständlich machen; für deinen ersten Patch ist jedoch die Frage wichtiger, welcher vorhandene Ereignishandler Daten verändert. Folge etwa buttonSave_Click bis zum SQL-Befehl und markiere Aufrufe von MessageBox.Show, Dateisystem, Registry und Netzwerk. Diese Seiteneffekte bestimmen, was ein Test kontrollieren muss. Ich würde nicht zuerst alle Formulare in eine neue Architektur überführen, weil du ohne Verhaltensnachweis nicht erkennen kannst, ob die Verschiebung einen alten Sonderfall entfernt hat.
Visual Studio 2022 und die Projektdatei zeigen dir außerdem, ob du tatsächlich auf .NET Framework 4.8 arbeitest oder bereits ein SDK-Projekt mit net8.0-windows vor dir hast. Diese Unterscheidung ist keine Geschmacksfrage: Ein Frameworkwechsel kann Paketabhängigkeiten, Deployment und Konfiguration berühren, während ein Test-Seam im bestehenden Ziel-Framework bleiben kann. Microsoft bezeichnet .NET 8 als LTS-Version mit drei Jahren Support; diese Herstellerangabe ist ein Argument für die Planung eines späteren Ports, aber kein Beleg dafür, dass er vor dem ersten Test stattfinden muss.
Miss vor dem Patch nur Werte, die du danach erneut erheben kannst. Eine sinnvolle Metrik ist die Zeit vom Klick auf „Speichern“ bis zur wieder bedienbaren Oberfläche, ergänzt um die Zahl der Schreibvorgänge und das Ergebnis nach einem Neustart. In einem fiktiven Messprotokoll könnten dafür 2,4 Sekunden und ein Datenbank-Update stehen; solche Beispielwerte sind ausdrücklich keine Aussage über deine Anwendung. Lege ProcMon mit einem Filter auf den Prozessnamen daneben, wenn Dateizugriffe oder Registry-Abhängigkeiten unklar sind: Das Werkzeug zeigt beobachtete Zugriffe, erklärt aber noch nicht, ob sie fachlich erforderlich sind.
Charakterisierungstests sind vor breiten GUI-Tests die bessere erste Wette
Ein Charakterisierungstest behauptet nicht, dass der Altcode richtig ist. Er hält fest, was ein ausgewählter Pfad gegenwärtig tut, damit eine Änderung sichtbar wird. Suche dafür einen Ausdruck im Ereignishandler, der ohne Fenster auskommt: Eingabeumwandlung, Berechnung oder Entscheidung über eine Fehlermeldung. Ziehe ihn unverändert in eine kleine Methode und lass den Ereignishandler diese Methode aufrufen. Vergleiche unmittelbar davor und danach dieselben Eingaben; andernfalls testest du womöglich eine neue Implementierung statt des geerbten Verhaltens.
Das folgende Beispiel ist eine lauffähige xUnit-Testdatei für ein vorhandenes .NET-Testprojekt mit xUnit-Referenz. Sie zeigt einen möglichen Seam für die Umwandlung eines Betrags. Übernimm de-DE nur, wenn die bisherige Anwendung diese Kultur tatsächlich verwendet; ein stiller Wechsel der Zahlenkultur wäre bereits eine fachliche Änderung.
using System.Globalization;
using Xunit;
public static class AmountBoundary
{
public static decimal Read(string text) =>
decimal.Parse(text, NumberStyles.Number, CultureInfo.GetCultureInfo("de-DE"));
}
public sealed class AmountBoundaryTests
{
[Theory]
[InlineData("1,50", "1.50")]
[InlineData("1.500,00", "1500.00")]
public void ReadsGermanInput(string text, string expected) =>
Assert.Equal(decimal.Parse(expected, CultureInfo.InvariantCulture), AmountBoundary.Read(text));
}
Führe den Test mit dotnet test aus, sofern die Solution SDK-Projekte verwendet; bei einer älteren .NET-Framework-Solution kann der passende Test-Runner in Visual Studio oder ein MSBuild-basierter Build nötig sein. Der Code beweist zunächst nur die Umwandlung. Erst wenn der bestehende Klick-Handler AmountBoundary.Read aufruft und ein kurzer manueller Durchlauf den Anschluss bestätigt, schützt er einen Teil des produktiven Pfads. Genau diese Anschlussstelle gehört in den Review: Ein bestandener Test für unbenutzten Code ist wertlos.
Hier stehen zwei Optionen gegeneinander. xUnit-Charakterisierungstests gewinnen, wenn die Fehlerursache in Eingaben, Berechnungen oder Entscheidungen liegt: Sie laufen ohne interaktive Windows-Sitzung und machen Varianten schnell prüfbar, kosten aber Zeit für eine vorsichtige Extraktion und sehen keine Fokusprobleme. FlaUI mit Microsoft UI Automation gewinnt, wenn der Verdacht auf Tab-Reihenfolge, Dialoge oder nicht erreichbare Bedienelemente fällt: Es prüft die tatsächliche Oberfläche, kostet aber einen stabilen Windows-Desktop, Testdaten und Pflege bei Layoutänderungen. Deshalb würde ich nicht mit einer vollständigen FlaUI-Suite starten, solange schon die fachliche Wirkung des Speichern-Klicks ungeprüft ist.
Wähle für den ersten Test keinen bequemen Normalfall allein. Ein leerer Wert, ein Wert mit Gruppentrennzeichen und eine Eingabe, die bisher eine Fehlermeldung auslöst, grenzen das Verhalten besser ein, weil sie unterschiedliche Zweige des Codes ausführen. Als zunächst verhandelbare Obergrenze reichen 3 solche Fälle für den ersten Seam; die Zahl soll den Patch klein halten, nicht Testvollständigkeit vortäuschen. Dokumentiere ausdrücklich, welche unerwartete Ausgabe du vorerst konservierst. Ein späterer Bugfix kann diese Erwartung bewusst ändern, statt sie nebenbei zu brechen.
Ein schmaler Adapter senkt das Risiko stärker als ein Frameworkwechsel
Nach dem ersten Test wird wahrscheinlich sichtbar, warum der Formularcode schwer prüfbar ist: Der Klick-Handler liest Textboxen, validiert Eingaben, schreibt per ADO.NET in SQL Server und zeigt eine MessageBox. Trenne zunächst nur die Grenze zum schwer kontrollierbaren Effekt. Eine kleine Schnittstelle für den konkreten Schreibvorgang und eine Implementierung, die den bisherigen ADO.NET-Aufruf enthält, genügen oft. Der Handler darf vorerst Handler bleiben; sein Aufrufpfad wird nachvollziehbarer, weil ein Test den Schreibvorgang durch einen kontrollierten Ersatz beobachten kann.
Die Hinweise in .NET WinForms Entwicklung: Tipps fuer stabile Desktop-Apps können zu robusteren Formularen anregen; ich würde daraus aber keinen Auftrag ableiten, sämtliche Events sofort umzubauen. Ein Adapter um SqlCommand ist leichter rückgängig zu machen als eine gleichzeitig geänderte Oberfläche, Datenzugriffsschicht und Laufzeitumgebung. Er ermöglicht außerdem eine präzise Frage: Wird beim ungültigen Betrag gar kein Schreibvorgang ausgelöst, und wird beim gültigen Betrag genau der erwartete Wert übergeben?
Verwechsle dabei einen Fake nicht mit einer Datenbankprüfung. Ein Fake belegt, welche Argumente der Handler weiterreicht; er belegt weder SQL-Syntax noch Transaktionsverhalten. Für diese zweite Grenze brauchst du einen separaten Integrationstest gegen eine isolierte SQL-Server-Testdatenbank oder SQL Server Express LocalDB, sofern die produktive Anwendung ebenfalls SQL Server verwendet. SQLite würde ich nicht allein wegen des einfachen Setups als Ersatz wählen, weil Typkonvertierung und SQL-Verhalten von der produktiven Datenbank abweichen können. Halte Testdaten und erwartete Zeilenänderung im Repository fest, aber niemals produktive Zugangsdaten.
Ebenso verlockend ist es, einen blockierenden Aufruf nebenbei mit Task.Run zu „reparieren“. Lass das zunächst, wenn du den Lebenszyklus des Formulars nicht kennst: Ein Hintergrundtask kann nach dem Schließen noch laufen und darf WinForms-Steuerelemente nicht direkt von seinem Thread aus ändern. Miss erst, ob die Oberfläche während des Schreibens tatsächlich blockiert, und trenne eine spätere Responsiveness-Änderung vom Adapter-Patch. Für diese spätere Änderung gehören Rückkehr auf den UI-Thread, Abbruch beim Schließen und Fehleranzeige jeweils in eigene überprüfbare Akzeptanzfälle.
Eine Freigabe braucht Belege aus derselben Laufzeitumgebung
Ein grüner Test auf dem Entwicklerrechner ist noch keine Freigabe, wenn die Anwendung beim Nutzer mit anderen Rechten, Konfigurationsdateien oder Datenbankverbindungen startet. Baue die unveränderte und die geänderte Version mit derselben MSBuild-Konfiguration und lasse beide auf einer definierten Windows-Testmaschine denselben Ablauf durchlaufen. Ein CI-Job auf windows-2022 eignet sich für Build und nichtinteraktive Tests; für UI-Automation kann dagegen eine eigens betriebene interaktive Sitzung nötig sein, weil ein CI-Prozess ohne Desktop echte Fenster nicht zuverlässig bedient.
Lege vor dem Vergleich fest, was als Abweichung zählt. Als einstellbaren Startwert kannst du für den Speichern-Ablauf ein Zeitbudget von 5 Sekunden festlegen; passe es anhand eurer Geräte und fachlichen Anforderungen an, statt es als universellen Grenzwert auszugeben. Für wiederholte Läufe ist das 95. Perzentil aussagekräftiger als ein einzelner Bestwert, weil seltene Hänger für Anwender spürbar bleiben. Protokolliere daneben Fehlertext, Exitcode und Datenbankzustand: Eine schnelle Oberfläche kann eine fehlgeschlagene Speicherung verdecken.
Wenn Abstürze auftreten, konfiguriere Windows Error Reporting über LocalDumps für den betroffenen Prozess und prüfe die Dumps nur in einer zugriffsbeschränkten Testumgebung, weil Speicherabbilder eingegebene Daten und Zugangsinformationen enthalten können. Das Windows-Ereignisprotokoll liefert ergänzend Zeitpunkt und Fehlermodul. Nimm diese Belege nicht als Ersatz für den Testfall: Ein Stacktrace zeigt die Absturzstelle, während ein reproduzierbarer Ablauf die Voraussetzungen sichtbar macht. Vermerke deshalb im Ticket sowohl den beobachteten Fehler als auch den kleinsten Eingabefall, der ihn auslöst.
Halte den Release bewusst umkehrbar. Eine Änderung an einer Methode, ihrem Aufruf im Handler und den zugehörigen Tests lässt sich mit Git gezielt prüfen und zurücknehmen; ein gemischter Commit aus Laufzeitport und Verhaltensänderung erschwert diese Entscheidung. Vor der Auslieferung sollte ein anderer Mensch den ursprünglichen Ablauf auf dem Release-Build ausführen. Das ist keine Forderung nach vollständiger manueller Regression, sondern eine gezielte Prüfung der Stelle, an der Test-Seam und echte Oberfläche zusammentreffen.
Der nächste Schritt ist ein protokollierter Speichern-Klick
Nimm morgen einen häufig genutzten Speichern-Ablauf und führe ihn auf einem festgehaltenen Commit einmal vollständig aus. Notiere Eingabe, sichtbare Reaktion und gespeicherten Wert. Markiere danach im Klick-Handler die erste reine Entscheidung, die du unverändert extrahieren kannst. Wenn du keinen solchen Ausdruck findest, dokumentiere stattdessen den ersten externen Aufruf. Beides liefert eine konkrete Grenze für einen kleinen Patch, ohne einen Rewrite vorauszusetzen.


