Planung von Tests
-
- vor 8 Jahren zuletzt von Romana bearbeitet
-
Inhaltsverzeichnis
Einleitung
Tests entwerfen ist eine einfache Sache, solange es sehr wenige Tests sind. Wird der zu testende Code aber umfangreicher, lohnt es sich, sich Gedanken zu machen, wie man die Aufgabe am besten löst. Tests sollten folgende Bedingungen erfüllen:
- Verlässlichkeit: Es sollten alle Fehler erkannt werden, aber false positives vermieden werden
- Wiederholbarkeit: Eigentlich eine Sache, die sich von selbst versteht, trotzdem der Vollständigkeit halber: Bei gleicher Ausgangssituation und gleichen Eingaben muss auch dasselbe Ergebnis heraus kommen.
- Vollständigkeit: Es sollte alles getestet werden, was nötig ist. Gerade bei Akzeptanz-Tests ist das eine Herausforderung, denn hier wird oft mit Code-/Programmteilen interagiert, die nicht zum eigentlichen Code gehören. Da müssen auch alle "Rahmenbedingungen" der Programmteile erfasst werden, die für diese Testreihe nötig sind.
- Schnelligkeit: Damit Tests gerne und oft verwendet werden, sollen sie schnell sein.
- Unabhängigkeit der Tests: Tests dürfen nicht voneinander abhängen! Wenn ein Test in einer Testreihe fehlschlägt, dann darf das keine Auswirkungen auf die weiteren Tests haben. Damit sorgt man dafür, dass die Testergebnisse verlässlich sind. Folgefehler werden ausgeschlossen und man erhält gleichzeitig genauere Rückmeldung, wo was schief läuft.
- Nur ein Item pro Test. Das ist etwas Definitionssache und muss gegebenenfalls für den nächsten Punkt passend definiert werden.
- DRY - Don't Repeat Yourself! Auch bei den Tests sollte natürlich im Hinblick auf die Wartbarkeit und Erweiterbarkeit der Tests mehrfach derselbe Code vermieden werden. Das lässt sich nicht immer machen, gerade bei Akzeptanz-Tests muss gelegentlich derselbe Test mit unterschiedlichen Parametern von außerhalb durchlaufen werden.
Man kann sich schließlich überlegen, ob man bei der Entwicklung von Software nicht zum Test Driven Design (TDD) übergeht. Das heißt in Kurzform: Erst Test(s) schreiben, dann die Software dazu. Immerhin geht es beim Programmieren ja darum, eine gewünschte Reaktion/ein gewünschtes Ergebnis zu erzielen, wenn eine bestimmte Aktion auftritt. Wenn man erst den Test schreibt, ist die Versuchung, sich zu verzetteln, viel geringer. Man bleibt auf das fokussiert, was man erreichen will. Man könnte dann auch sagen, dass der/die Test(s) die Spezifikation dessen ist, was die Software erreichen will.
Planung der Tests
Die hier beschriebene Vorgehensweise ist vielleicht nicht das Nonplusultra, aber mir hilft es enorm. Dabei entspricht die Reihenfolge im Großen und Ganzen auch dem Ablauf, wie die Tests am besten geplant werden. Allerdings kann man davon ausgehen, dass alle Schritte immer wieder überarbeiten werden müssen, weil in den Arbeitsschritten später Erkenntnisse kommen, die man einfach vorher noch nicht berücksichtigen kann. So kann man zum Beispiel die Bestimmung der Startpunkte erst dann als abgeschlossen betrachten, wenn wirklich alle Tests notiert und gruppiert sind.
Das ist eine iterative Vorgehensweise ganz im Sinne der agilen Programmierung.
Startpunkte der Tests bestimmen
Hier werden die Basis-Datenbestände der Testdaten definiert. Diese bestehen aus Ordnern, Dateien, Datenbanken, Tabellen und Daten in den Tabellen.
Während die Tests laufen, werden diese Daten natürlich manipuliert. Folglich muss man dafür sorgen, dass für jeden einzelnen Test immer wieder dieselbe Ausgangssituation verfügbar ist. Das ist insbesondere wichtig, wenn ein vorausgegangener Test fehlschlägt (Unabhängigkeit der Tests).
Zur Bestimmung, wie viele Startpunkte es für die Testreihen gibt, also wie viele Basis-Datenbestände man braucht, hilft es, zu überlegen, bei welchen Tests Ordner, Dateien oder Tabellen hinzu kommen oder weg fallen. Die Wiederherstellung solcher Ausgangszustände ist aufwändiger, als wenn nur dafür gesorgt werden muss, dass die verwendeten Tabellen immer dieselben Ausgangsdaten haben.
Im Beispiel ist das ein Datenbestand, der den Abonnenten, der während der Tests verwendet wird, nicht bei den bereits vorhandenen Abonnenten ist. Außerdem wird dafür gesorgt, dass die Optionen von Komponente und der beiden Plugins immer einen definierten Ausgangszustand haben.
Warum mehrere Startpunkte?
Tests sollen so schnell wie möglich laufen, damit sie auch gerne verwendet werden!
Gerade wenn man wie ich Erweiterungen zu einem größeren System schreibt, in meinem Fall Joomla, kommen auch noch die Installation, das Bestücken mit Daten und die Deinstallation "erschwerend" hinzu. Diesen Teil kann man in einen weiteren Test-Bereich verschieben, der dann Integrationstests genannt wird. Für die Arbeit am Code, dann prüfen, korrigieren oder weitere Änderungen, ist es nicht nötig, dass jedes Mal neu installiert und deinstalliert wird.
Da die meisten Tests zur Funktionalität nicht nur am Stück laufen sondern auch für Tests von Teilbereichen verwendet werden, macht es wenig Sinn, immer auch die Installation mit zu testen. Daher lege ich bei den Tests gerne mehrere Startpunkte an.
Parameter für die Tests notieren
Natürlich müssen bestimmte Optionen von Komponente und Plugins verändert werden, dass alles abgedeckt wird, was im normalen Betrieb auftreten kann. Hier werden die Optionen erfasst, die für das Plugin von Bedeutung sind. Es wird überlegt, welche Einstellungen getestet werden müssen (zum Beispiel Pflichtangabe oder nicht). Das hängt nun tatsächlich auch wieder von der Testsituation ab. Teste ich das Plugin Buyer2Subscriber, dann ist es gleichgültig, was im der Komponente als Pflicht oder auch nur Anzeige von Vor- und Nachnamen eingestellt ist, denn Virtuemart liefert diese Daten immer, weil in Virtuemart ja eine Rechnung erstellt werden muss und diese Angaben dafür braucht. Teste ich allerdings das Plugin User2Subscriber, dann "ziehen" diese Einstellungen wohl! Hier sind die Test-Parameter also die entsprechenden Optionen der Komponente und der Plugins.
Es muss auch die Situation getestet werden, dass ein Abonnement für den Abonnenten vorhanden ist. Dieses vorhandene Abonnement kann natürlich auch wieder unterschiedliche Werte besitzen, die möglicherweise durch das Plugin geändert werden können, geändert werden dürfen oder eben nicht.
Alle Testfälle erfassen
Weiß man, von welcher Situation man ausgehen kann und was alles an Variationen möglich ist, kann man alle Testfälle erfassen. Sinnvollerweise gruppiert man die Tests erst mal so, dass immer nur ein Parameter verändert und alles andere beibehalten wird.
Auf der Seite zu Buyer2Susbcriber sind die Testfälle für das Plugin Buyer2Subscriber für BwPostman erfasst, hoffentlich bereits vollständig. Dies ist die Sortierung 1.
Sortieren der Tests nach Items
Die erste Sortierung ist ein Paradebeispiel dafür, wie DRY mit Füßen getreten wird.
Jetzt kann man sich überlegen, welche Testfälle zusammengefasst werden können, um DRY möglichst gut erfüllen zu können, ohne die Bedingung nur ein Item pro Test zu verletzen. Hierfür ist eine sinnvolle Definition nötig, was ein Item überhaupt ist.
Tests schreiben
Es bietet sich hier die Top-Down-Methode an, also auch wieder eine iterative Vorgehensweise ganz im Sinne der agilen Programmierung.
Das heißt, dass man erst mal eine leere Hülle für die Tests anlegt. Für jeden Test eine Methode, die nur den Namen und eine Beschreibung enthält. Es wird beschrieben, was getestet wird und welches Ergebnis erwartet wird. In Codeception wird das mittels
$I->wantTo('was mache ich in diesem Test');
$I->expectTo('was für ein Ergebnis erwarte ich von diesem Test');
umgesetzt.
Dann werden die Tests in einzelne Schritte zerlegt. Diese Einzelschritte sind erst mal nur Aufrufe von Hilfsmethoden. Auf diese Weise bekommt man schnell ein Gespür dafür, was in einer Hilfsmethode erledigt werden kann, so dass sie bei mehreren Tests verwendet werden kann. Dieses Zerlegen in Einzelschritte kann immer weiter nach unten durchgeführt werden, bis man eine Stufe erreicht hat, wo ein sinnvolles Zerlegen nicht mehr möglich ist.
Hier kann man auch erkennen, ob die Sortierung und Gruppierung der Tests sinnvoll ist oder vielleicht noch mal überarbeitet werden muss.
Das mehrfache Verwenden von Hilfsmethoden kann so weit gehen, dass man die Hilfsmethoden in eigene Klassen auslagert, damit sie von anderen Testsuiten ebenfalls verwendet werden können. Im Beispiel gibt es die Hilfsmethode Bestehenden Abonnenten anlegen. (Ja, kling etwas komisch, dass man etwas anlegen muss, was schon besteht.) Diese Methode ist nicht nur im Plugin Buyer2Subscriber nützlich. Sie kann ebenfalls bei den Tests für das Plugin User2Subscriber, das Modul zur Anmeldung und selbst bei Komponente bei den Tests zur Anmeldung verwendet werden, denn in all diesen Situationen muss ja auch überprüft werden, wie sich "das System" verhält, wenn jemand ein Abonnement haben will, diese Mailadresse (die der Dreh- und Angelpunkt und der einzig verlässliche Indikator für einen Abonnenten in ganz BwPostman ist) BwPostman aber schon bekannt ist, also der potentielle/neue Abonnent bereits ein Abonnement hat.
Bei den Tests ist es eben wie im echten Programmierer-Leben: Gut durchdacht hilft es, DRY einzuhalten. Tests (genauer: automatische Tests) sind ja nichts anderes als Code, der geschrieben und ausgeführt wird. Manchmal macht es Sinn, manchmal sogar viel Sinn, seine Vorgehensweise so anzupassen oder abzuändern, dass man möglichst viel in einfache und leicht bedienbare Hilfsmethoden "auslagert".
Tests testen
Ja, auch das gehört dazu! Wie will man behaupten, dass die Tests verlässlich sind, wenn man sie nicht getestet hat?
Tests optimieren
Und noch ein Punkt, der besondere Aufmerksamkeit verlangt. Tests sind wie jeder andere Code, den man schreibt, zu behandeln. Auch hier kann man sehen, ob man den Code nicht optimieren kann im Sinne von DRY. Also kleinere Methoden anlegen und so gestalten, dass sie immer wieder verwendet werden können. Dann auch auf die Lesbarkeit des Codes achten. Und schließlich: Die Laufzeit!
Hier kann man immer wieder kontrollieren, ob man nicht Dinge mit testet, die an anderer Stelle bereits getestet werden und hier nur Zeit kosten.
Ein Beispiel hierzu:
Ich habe für das Plugin Buyer2Subscriber einen Test, der erst mal dafür sorgt, dass ich einen sauberen Ausgangszustand habe (wenn die Tests am Stück laufen, dann kann es durchaus passieren, dass der vorausgegangene Test fehl schlägt und Leichen hinterlässt). Dann wird das Registrierungsformular im Frontend ausfüllt und abschickt, dann im Backend erst mal nachgesehen, ob der Account angelegt wurde, der Abonnent als unbestätigt eingetragen ist. Dann wird wieder ins Frontend gewechselt und der Account bestätigt. Jetzt muss ich natürlich wissen, ob der Account bestätigt und vor allem, ob das Abonnement bestätigt wurde, also der Abonnent nun bei den bestätigten gelistet ist. Anschließend wird noch aufgeräumt.
Ich hatte das in der ersten Version alles mit einer "Klick-Orgie" in Codeception gelöst. Damit läuft dieser Test 48 Sekunden.
Als erstes habe ich die Kontrolle, ob der Account und der Abonnent beim Aufräumen auch wirklich gelöscht werden, auf einfache Abfragen der Datenbank reduziert. Ergebnis: Laufzeit 44 Sekunden.
Als nächstes habe ich die Erfolgskontrolle, also ob der Account und das Abonnement "angekommen" sind, auf Datenbankabfrage umgestellt. Das wird bereits bei den Listentests geprüft! Ergebnis: Laufzeit 34 Sekunden!
Als drittes habe ich das Löschen von Account und Abonnement auf direktes Löschen in der Datenbank umgestellt. Auch das wird im Listentest bereits geprüft. Ergebnis: Laufzeit 28 Sekunden!
Das ist eine Verkürzung der Laufzeit um rund 42% und ich bin noch nicht fertig. Wenn man bedenkt, dass es für dieses Plugin im Moment fast 40 Tests gibt und man das hoch rechnet…
Tests freigeben
Vielleicht ist das nur einfach einen Haken auf einer ToDo-Liste, vielleicht ist es ein Einpflegen in sein System für Continuous Delivery, ein Eintrag in Jenkins oder was auch immer. Jedenfalls ist das im Sinne von DevOps eine Auslieferung an den Kunden in diesem Fall ist der Kunde die Programmierung.