Alles hat eine Schnittstelle. Wenn ich meinen Testhut aufsetze, verwende ich eine bestimmte Weltanschauung, um einen Test zu schreiben:
- Wenn etwas existiert, kann es gemessen werden.
- Wenn es nicht gemessen werden kann, spielt es keine Rolle. Wenn es wichtig ist, habe ich noch keinen Weg gefunden, es zu messen.
- Anforderungen schreiben messbare Eigenschaften vor oder sind nutzlos.
- Ein System erfüllt eine Anforderung, wenn es von einem nicht erwarteten Zustand in den durch die Anforderung vorgeschriebenen erwarteten Zustand übergeht.
- Ein System besteht aus interagierenden Komponenten, die Subsysteme sein können. Ein System ist korrekt, wenn alle Komponenten korrekt sind und die Interaktion zwischen den Komponenten korrekt ist.
In Ihrem Fall besteht Ihr System aus drei Hauptteilen:
- eine Art von Daten oder Bildern, die aus Dateien initialisiert werden können
- ein Mechanismus zum Anzeigen der Daten
- ein Mechanismus zum Ändern der Daten
Das klingt für mich übrigens sehr nach der ursprünglichen Model-View-Controller-Architektur. Im Idealfall weisen diese drei Elemente eine lose Kopplung auf - das heißt, Sie definieren klare Grenzen zwischen ihnen mit genau definierten (und damit gut testbaren) Schnittstellen.
Eine komplexe Interaktion mit der Software kann in kleine Schritte übersetzt werden, die in Bezug auf die Elemente des Systems, das wir testen, formuliert werden können. Zum Beispiel:
Ich lade eine Datei mit einigen Daten. Es wird ein Diagramm angezeigt. Wenn ich einen Schieberegler in die Benutzeroberfläche ziehe, wird das Diagramm wackelig.
Dies scheint einfach manuell und automatisiert zu testen zu sein. Aber lassen Sie uns diese Geschichte in unser System übersetzen:
- Die Benutzeroberfläche bietet einen Mechanismus zum Öffnen einer Datei: Der Controller ist korrekt.
- Wenn ich eine Datei öffne, gibt der Controller einen entsprechenden Befehl an das Modell aus: Die Interaktion zwischen Controller und Modell ist korrekt.
- Bei einer gegebenen Testdatei analysiert das Modell diese in die erwartete Datenstruktur: Das Modell ist korrekt.
- Bei einer Testdatenstruktur rendert die Ansicht die erwartete Ausgabe: Die Ansicht ist korrekt. Einige Testdatenstrukturen sind normale Diagramme, andere wackelige Diagramme.
- Die Interaktion Ansicht - Modell ist korrekt
- Die Benutzeroberfläche bietet einen Schieberegler, mit dem das Diagramm wackelig wird: Der Controller ist korrekt.
- Wenn der Schieberegler auf einen bestimmten Wert eingestellt ist, gibt der Controller den erwarteten Befehl an das Modell aus: Die Interaktion zwischen Controller und Modell ist korrekt.
- Wenn ein Testbefehl bezüglich Wobbeligkeit empfangen wird, transformiert das Modell eine Testdatenstruktur in die erwartete Ergebnisdatenstruktur.
Nach Komponenten gruppiert, erhalten wir die folgenden zu testenden Eigenschaften:
- Modell:
- analysiert Dateien
- reagiert auf den Befehl zum Öffnen von Dateien
- bietet Zugriff auf Daten
- reagiert auf wackeligen Befehl
- Aussicht:
- Regler:
- Bietet einen Workflow zum Öffnen von Dateien
- gibt den Befehl zum Öffnen der Datei aus
- Bietet einen wackeligen Workflow
- gibt einen wackeligen Befehl aus
- das ganze System:
- Die Verbindung zwischen den Komponenten ist korrekt.
Wenn wir das Problem des Testens nicht in kleinere Untertests zerlegen, wird das Testen sehr schwierig und sehr fragil. Die obige Geschichte könnte auch implementiert werden als "Wenn ich eine bestimmte Datei lade und den Schieberegler auf einen bestimmten Wert setze, wird ein bestimmtes Bild gerendert". Dies ist fragil, da es kaputt geht, wenn sich ein Element im System ändert.
- Es bricht ab, wenn ich die Steuerelemente auf Wackeligkeit ändere (z. B. Ziehpunkte in der Grafik anstelle eines Schiebereglers in einem Bedienfeld).
- Es bricht ab, wenn ich das Ausgabeformat ändere (z. B. unterscheidet sich die gerenderte Bitmap, weil ich die Standardfarbe des Diagramms geändert habe oder weil ich Anti-Aliasing hinzugefügt habe, um das Diagramm glatter aussehen zu lassen. Beachten Sie dies in beiden Fällen).
Granulare Tests haben auch den großen Vorteil, dass ich das System weiterentwickeln kann, ohne befürchten zu müssen, dass eine Funktion beschädigt wird. Da alle erforderlichen Verhaltensweisen von einer vollständigen Testsuite gemessen werden, werden mich die Tests benachrichtigen, falls etwas kaputt geht. Da sie körnig sind, weisen sie mich auf den Problembereich hin. Wenn ich beispielsweise versehentlich die Schnittstelle einer Komponente ändere, schlagen nur die Tests dieser Schnittstelle fehl und kein anderer Test, der diese Schnittstelle indirekt verwendet.
Wenn das Testen einfach sein soll, erfordert dies ein geeignetes Design. Zum Beispiel ist es problematisch, wenn ich Komponenten in einem System fest verdrahtete: Wenn ich die Interaktion einer Komponente mit anderen Komponenten in einem System testen möchte, muss ich diese anderen Komponenten durch Teststubs ersetzen, mit denen ich protokollieren, überprüfen kann, und choreografiere diese Interaktion. Mit anderen Worten, ich benötige einen Mechanismus zur Abhängigkeitsinjektion, und statische Abhängigkeiten sollten vermieden werden. Beim Testen einer Benutzeroberfläche ist es eine große Hilfe, wenn diese Benutzeroberfläche skriptfähig ist.
Natürlich ist das meiste davon nur eine Fantasie einer idealen Welt, in der alles entkoppelt und leicht zu testen ist und fliegende Einhörner Liebe und Frieden verbreiten ;-) Während alles grundsätzlich testbar ist, ist es oft unerschwinglich schwierig, dies zu tun, und Sie haben es besser Nutzung Ihrer Zeit. Systeme können jedoch auf Testbarkeit ausgelegt werden, und in der Regel verfügen sogar testunabhängige Systeme über interne APIs oder Verträge, die getestet werden können (wenn nicht, wette ich, dass Ihre Architektur Mist ist und Sie einen großen Schlammball geschrieben haben). Nach meiner Erfahrung bewirken bereits geringe Mengen (automatisierter) Tests eine spürbare Qualitätssteigerung.