Es gibt viele Tools im PHP-Ökosystem, die bequemes PHP-Testen ermöglichen. Eines der bekanntesten ist
PHPUnit , was beinahe ein Synonym für das Testen in dieser Sprache ist. Über gute Testmethoden wird jedoch nicht viel geschrieben. Es gibt viele Möglichkeiten, warum und wann man Tests schreibt, welche Art von Tests und so weiter.
Um ehrlich zu sein, macht es
keinen Sinn, einen Test zu schreiben, wenn Sie ihn später nicht lesen können .
Tests sind eine besondere Art der Dokumentation. Wie
ich bereits in PHP über TDD geschrieben habe , wird im Test immer (oder zumindest sollte) klar angegeben, welche Aufgabe ein bestimmtes Stück Code hat.
Wenn ein Test diese Idee nicht ausdrücken kann, ist der Test schlecht.
Ich habe eine Reihe von Techniken vorbereitet, mit denen PHP-Entwickler gute, lesbare und nützliche Tests schreiben können.
Beginnen wir mit den Grundlagen
Es gibt eine Reihe von Standardtechniken, denen viele ohne Fragen folgen. Ich werde viele von ihnen erwähnen und versuchen zu erklären, warum sie gebraucht werden.
1. Tests sollten keine Eingabe-Ausgabe-Operationen enthalten
Der Hauptgrund : E / A-Vorgänge sind langsam und unzuverlässig.
Langsam : Auch wenn Sie über die beste Hardware der Welt verfügen, ist die E / A-Verarbeitung langsamer als der Speicherzugriff. Tests sollten immer schnell funktionieren, sonst werden sie zu selten ausgeführt.
Unzuverlässig : Einige Dateien, Binärdateien, Sockets, Ordner und DNS-Einträge sind möglicherweise auf einigen Computern, auf denen Sie testen, nicht verfügbar. Je mehr Sie sich auf das Testen von E / A verlassen, desto stärker sind Ihre Tests an die Infrastruktur gebunden.
Welche Operationen beziehen sich auf I / O:
- Dateien lesen und schreiben.
- Netzwerkanrufe.
- Aufrufe an externe Prozesse (mit
exec
, proc_open
usw.).
Es gibt Situationen, in denen das Vorhandensein von Eingabe- / Ausgabeoperationen es Ihnen ermöglicht, Tests schneller zu schreiben. Aber seien Sie vorsichtig: Vergewissern Sie sich, dass diese Vorgänge bei der Entwicklung, Montage und Bereitstellung auf Ihren Maschinen gleich funktionieren, da Sie sonst möglicherweise ernsthafte Probleme haben.
Isolieren Sie die Tests so, dass sie keine E / A-Vorgänge benötigen: Im Folgenden finden Sie eine Architekturlösung, die verhindert, dass Tests E / A-Vorgänge ausführen, indem die Verantwortung zwischen den Schnittstellen aufgeteilt wird.
Ein Beispiel:
public function getPeople(): array { $rawPeople = file_get_contents( 'people.json' ) ?? '[]'; return json_decode( $rawPeople, true ); }
Wenn Sie mit dem Testen mit dieser Methode beginnen, wird eine lokale Datei erstellt und von Zeit zu Zeit werden Snapshots erstellt:
public function testGetPeopleReturnsPeopleList(): void { $people = $this->peopleService ->getPeople();
Dazu müssen die Voraussetzungen für die Ausführung der Tests konfiguriert werden. Auf den ersten Blick sieht alles vernünftig aus, aber in Wirklichkeit ist es schrecklich.
Das Überspringen eines Tests, weil die Voraussetzungen nicht erfüllt sind, garantiert nicht die Qualität unserer Software. Dies wird nur Fehler verbergen!
Wir beheben die Situation : Wir isolieren E / A-Operationen, indem wir die Verantwortung auf die Schnittstelle verlagern.
Jetzt weiß ich, dass
JsonFilePeopleProvider
auf jeden Fall I / O verwenden wird.
Anstelle von
file_get_contents()
Sie eine Abstraktionsebene wie
das Flysystem-Dateisystem verwenden , für das es einfach ist, Stubs zu
erstellen .
Und warum brauchen wir dann
PeopleService
? Gute Frage. Dazu sind Tests erforderlich: um die Architektur herauszufordern und nutzlosen Code zu entfernen.
2. Tests sollten bewusst und aussagekräftig sein.
Der Hauptgrund : Tests sind eine Form der Dokumentation. Halten Sie sie klar, präzise und lesbar.
Klarheit und Kürze : kein Durcheinander, keine tausend Stichleitungen, keine Abfolge von Aussagen.
Lesbarkeit : Tests sollten eine Geschichte erzählen. Die gegebene, wann, dann-Struktur ist dafür hervorragend.
Merkmale eines guten und lesbaren Tests:
- Enthält nur die erforderlichen Aufrufe der
assert
Methode (vorzugsweise eine).
- Er erklärt sehr deutlich, was unter gegebenen Bedingungen geschehen soll.
- Es wird nur ein Zweig der Methodenausführung getestet.
- Um einer Aussage willen macht er keinen Stummel für das ganze Universum.
Es ist wichtig zu beachten, dass, wenn Ihre Implementierung bedingte Ausdrücke, Übergangsoperatoren oder Schleifen enthält, diese alle explizit durch Tests abgedeckt werden sollten. Zum Beispiel, damit frühe Antworten immer einen Test enthalten.
Ich wiederhole: Es geht nicht um Berichterstattung, sondern um Dokumentation.
Hier ist ein Beispiel für einen verwirrenden Test:
public function testCanFly(): void { $noWings = new Person(0); $this->assertEquals( false, $noWings->canFly() ); $singleWing = new Person(1); $this->assertTrue( !$singleWing->canFly() ); $twoWings = new Person(2); $this->assertTrue( $twoWings->canFly() ); }
Passen wir das Format "gegeben wann, dann" an und sehen, was passiert:
public function testCanFly(): void {
Wie im Abschnitt "Given" können "when" und "then" auf private Methoden übertragen werden. Dadurch wird Ihr Test besser lesbar.
assertEquals
sinnloses Durcheinander. Die Person, die dies liest, muss die Aussage verfolgen, um zu verstehen, was es bedeutet.
Durch die Verwendung bestimmter Anweisungen wird Ihr Test viel lesbarer.
assertTrue()
sollte eine Boolesche Variable erhalten, keinen Ausdruck wie
canFly() !== true
.
Im vorherigen Beispiel ersetzen wir
assertEquals
zwischen
false
und
$person->canFly()
durch eine einfache
assertFalse
:
Jetzt ist alles sehr klar! Wenn ein Mensch keine Flügel hat, darf er nicht fliegen können! Lesen Sie wie ein Gedicht
Nun ist der Abschnitt „Weitere Fälle“, der in unserem Text zweimal vorkommt, ein klares Indiz dafür, dass der Test zu viele Aussagen macht. Die
testCanFly()
-Methode ist völlig nutzlos.
Lassen Sie uns den Test noch einmal verbessern:
public function testCanFlyIsFalsyWhenPersonHasNoWings(): void { $person = $this->givenAPersonHasNoWings(); $this->assertFalse( $person->canFly() ); } public function testCanFlyIsTruthyWhenPersonHasTwoWings(): void { $person = $this->givenAPersonHasTwoWings(); $this->assertTrue( $person->canFly() ); }
Wir können die
testPersonCantFlyWithoutWings
sogar so umbenennen, dass sie mit dem tatsächlichen Szenario übereinstimmt, z. B. in
testPersonCantFlyWithoutWings
, aber mir passt
testPersonCantFlyWithoutWings
alles.
3. Der Test sollte nicht von anderen Tests abhängen
Der Hauptgrund : Tests sollten in beliebiger Reihenfolge erfolgreich ausgeführt werden.
Ich sehe keine ausreichenden Gründe, um Verbindungen zwischen Tests herzustellen. Kürzlich wurde ich gebeten, einen Login-Funktionstest durchzuführen, den ich hier als gutes Beispiel geben werde.
Der Test sollte:
- Generieren Sie ein JWT-Token für die Anmeldung.
- Führen Sie die Login-Funktion aus.
- Genehmigen Sie die Statusänderung.
Es war so:
public function testGenerateJWTToken(): void {
Das ist aus mehreren Gründen schlecht:
- PHPUnit kann diese Ausführungsreihenfolge nicht garantieren.
- Tests müssen unabhängig voneinander ausgeführt werden können.
- Parallele Tests können zufällig fehlschlagen.
Der einfachste Weg, dies zu umgehen, besteht darin, das gegebene when then-Schema zu verwenden. Damit die Tests nachdenklicher werden, werden sie eine Geschichte erzählen, ihre Abhängigkeiten klar demonstrieren und die getestete Funktion erklären.
public function testAmazingFeatureChangesState(): void {
Wir müssen auch Tests für die Authentifizierung usw. hinzufügen. Diese Struktur ist so gut, dass
standardmäßig Behat verwendet wird .
4. Implementieren Sie immer Abhängigkeiten
Der Hauptgrund : ein sehr schlechter Ton - um einen Stummel für den globalen Staat zu schaffen. Die Unfähigkeit, Stubs für Abhängigkeiten zu erstellen, ermöglicht es nicht, die Funktion zu testen.
Nützlicher Hinweis:
Vergessen Sie statische stateful-Klassen und Singleton-Instanzen . Wenn Ihre Klasse von etwas abhängt, machen Sie es so, dass es implementiert werden kann.
Hier ist ein trauriges Beispiel:
class FeatureToggle { public function isActive( Id $feature ): bool { $cookieName = $feature->getCookieName();
Wie kann ich diese frühe Antwort testen?
Alles ist richtig. Auf keinen Fall.
Um dies zu testen, müssen wir das Verhalten der Klasse "
Cookies
verstehen und sicherstellen, dass wir alle damit verbundenen Umgebungen reproduzieren können, was zu bestimmten Antworten führt.
Mach das nicht.
Die Situation kann korrigiert werden, wenn Sie eine Instanz von
Cookies
als Abhängigkeit implementieren. Der Test sieht folgendermaßen aus:
Gleiches gilt für Singletones. Wenn Sie also ein Objekt eindeutig machen möchten, konfigurieren Sie den Abhängigkeitsinjektor korrekt, anstatt das (Anti) Singleton-Muster zu verwenden. Andernfalls schreiben Sie Methoden, die nur für Fälle wie
reset()
oder
setInstance()
nützlich sind. Meiner Meinung nach ist das verrückt.
Es ist völlig normal, die Architektur zu ändern, um das Testen zu vereinfachen. Und Methoden zu entwickeln, die das Testen erleichtern, ist nicht normal.
5. Testen Sie niemals geschützte / private Methoden
Der Hauptgrund : Sie beeinflussen die Art und Weise, in der wir Funktionen testen, indem sie die Signatur des Verhaltens bestimmen. Unter einer solchen Bedingung erwarte ich, dass ich bei Eingabe von A B erhalte.
Private / geschützte Methoden sind nicht Teil der Funktionssignaturen .
Ich möchte nicht einmal einen Weg zeigen, um private Methoden zu "testen", aber ich gebe einen Hinweis: Sie können dies nur mit der
Reflection- API tun.
Bestrafen Sie sich immer irgendwie, wenn Sie daran denken, private Methoden mit Reflektion zu testen! Schlechter, schlechter Entwickler!
Private Methoden werden per Definition nur intern aufgerufen. Das heißt, sie sind nicht öffentlich verfügbar. Dies bedeutet, dass nur öffentliche Methoden derselben Klasse solche Methoden aufrufen können.
Wenn Sie alle Ihre öffentlichen Methoden getestet haben, haben Sie auch alle privaten / geschützten Methoden getestet . Ist dies nicht der Fall, entfernen Sie private / geschützte Methoden frei, sie werden von niemandem verwendet.
Fortgeschrittene Tipps
Ich hoffe dir ist noch nicht langweilig. Trotzdem musste ich über die Grundlagen sprechen. Jetzt teile ich meine Meinung zum Schreiben sauberer Tests und Entscheidungen, die sich auf meinen Entwicklungsprozess auswirken.
Das Wichtigste, das ich beim Schreiben von Tests nicht vergesse:
- Studie.
- Schnelles Feedback.
- Dokumentation
- Refactoring
- Design während des Testens.
1. Tests am Anfang, nicht am Ende
Werte : Studie, schnelles Feedback, Dokumentation, Umgestaltung, Design während des Tests.
Das ist die Basis von allem. Der wichtigste Aspekt, der alle aufgelisteten Werte beinhaltet. Wenn Sie Tests im Voraus schreiben, können Sie zunächst nachvollziehen, wie das Schema „Gegeben, Wann, Dann“ aufgebaut sein sollte. Dabei dokumentieren Sie zunächst Ihre Anforderungen und legen sie vor allem als wichtigste Aspekte fest.
Ist es seltsam, Tests vor der Implementierung zu schreiben? Und stellen Sie sich vor, wie seltsam es ist, etwas zu implementieren, und wenn Sie testen, um herauszufinden, ob all Ihre Ausdrücke "gegeben, wenn, dann" keinen Sinn ergeben.
Dieser Ansatz überprüft auch Ihre Erwartungen alle zwei Sekunden. Sie erhalten so schnell wie möglich eine Rückmeldung. Egal wie groß oder klein das Feature aussieht.
Grüne Tests sind ein idealer Bereich für die Umgestaltung. Die Grundidee: keine Tests - kein Refactoring. Refactoring ohne Tests ist einfach gefährlich.
Wenn Sie schließlich die Struktur auf "gegeben, wann" setzen, wird Ihnen klar, welche Schnittstellen Ihre Methoden haben sollten und wie sie sich verhalten sollten. Wenn Sie den Test sauber halten, müssen Sie auch ständig andere architektonische Entscheidungen treffen. Dies zwingt Sie dazu, Fabriken zu erstellen, Schnittstellen zu erstellen, die Vererbung zu unterbrechen usw. Und ja, das Testen wird einfacher!
Wenn es sich bei Ihren Tests um Live-Dokumente handelt, in denen die Funktionsweise der Anwendung erläutert wird, muss dies unbedingt deutlich gemacht werden.
2. Besser ohne Tests als mit schlechten Tests
Werte : Studie, Dokumentation, Refactoring.
Viele Entwickler denken über Tests folgendermaßen: Ich werde ein Feature schreiben, das Testframework so lange ansteuern, bis die Tests eine bestimmte Anzahl neuer Zeilen abdecken, und sie in Betrieb nehmen.
Es scheint mir, dass Sie der Situation mehr Aufmerksamkeit schenken müssen, wenn ein neuer Entwickler anfängt, mit dieser Funktion zu arbeiten.
Was sagen die Tests dieser Person?Tests sind oft verwirrend, wenn die Namen nicht detailliert genug sind. Was ist klarer:
testCanFly
oder
testCanFlyReturnsFalseWhenPersonHasNoWings
?
Wenn Ihre Tests nur chaotischer Code sind, der das Framework mehr Zeilen abdeckt, mit Beispielen, die keinen Sinn ergeben, ist es Zeit, anzuhalten und darüber nachzudenken, ob diese Tests überhaupt geschrieben werden sollen.
Sogar Unsinn wie das Zuweisen von
$a
und
$b
Variablen oder das Zuweisen von Namen, die sich nicht auf eine bestimmte Verwendung beziehen.
Denken Sie
daran : Bei Ihren Tests handelt es sich um Live-Dokumente, in denen das Verhalten Ihrer Anwendung erläutert wird.
assertFalse($a->canFly())
dokumentiert nicht viel. Und
assertFalse($personWithNoWings->canFly())
ist schon ziemlich viel.
3. Führen Sie die Tests intrusiv durch
Werte : Studie, schnelles Feedback, Refactoring.
Führen Sie die Tests aus, bevor Sie mit der Arbeit an Features beginnen. Wenn sie fehlschlagen, bevor Sie sich an die Arbeit machen, werden Sie darüber informiert,
bevor Sie den Code schreiben, und Sie müssen keine kostbaren Minuten damit verbringen, fehlerhafte Tests zu debuggen, die Sie nicht einmal interessiert haben.
Führen Sie nach dem Speichern der Datei die Tests aus. Je früher Sie feststellen, dass etwas kaputt ist, desto schneller können Sie es reparieren und weitermachen. Wenn eine Unterbrechung des Workflows zur Lösung eines Problems für Sie unproduktiv erscheint, stellen Sie sich vor, dass Sie später viele Schritte zurückgehen müssen, wenn Sie das Problem nicht kennen.
Führen Sie die Tests aus, nachdem Sie sich fünf Minuten mit Kollegen unterhalten oder Benachrichtigungen von Github überprüft haben. Wenn sie rot werden, wissen Sie, wo Sie aufgehört haben. Wenn die Tests grün sind, können Sie weiterarbeiten.
Führen Sie nach jedem Refactoring, auch nach Variablennamen, die Tests durch.
Im Ernst, führen Sie die verdammten Tests durch. So oft Sie die Speichertaste drücken.
PHPUnit Watcher kann dies für Sie tun und sogar Benachrichtigungen senden!
4. Große Tests - große Verantwortung
Werte : Studie, Umgestaltung, Design während des Testens.
Im Idealfall sollte jede Klasse einen Test haben. Dieser Test sollte alle öffentlichen Methoden in dieser Klasse sowie alle bedingten Ausdrücke oder Übergangsoperatoren abdecken ...
Sie können so etwas nehmen:
- Eine Klasse = ein Testfall.
- Eine Methode = ein oder mehrere Tests.
- Ein alternativer Zweig (if / switch / try-catch / exception) = ein Test.
Für diesen einfachen Code benötigen Sie also vier Tests:
Je mehr öffentliche Methoden Sie haben, desto mehr Tests werden benötigt.
Niemand liest gerne lange Dokumentationen. Da es sich bei Ihren Tests auch um Dokumente handelt, erhöhen die geringe Größe und Aussagekraft nur deren Qualität und Nützlichkeit.
Es ist auch ein wichtiges Signal dafür, dass Ihre Klasse Verantwortung ansammelt, und es ist an der Zeit, sie neu zu strukturieren, indem Sie eine Reihe von Funktionen auf andere Klassen übertragen oder das System neu gestalten.
5. Unterstützen Sie eine Reihe von Tests, um Regressionsprobleme zu lösen
Werte : Studie, Dokumentation, schnelles Feedback.
Betrachten Sie die Funktion:
function findById(string $id): object { return fromDb((int) $id); }
Sie denken, dass jemand "10" überträgt, aber tatsächlich "10 Bananen" übertragen werden. Das heißt, zwei Werte kommen, aber einer ist überflüssig. Du hast einen Bug.
Was wirst du zuerst tun? Schreiben Sie einen Test, der ein solches Verhalten als fehlerhaft markiert !!!
public function testFindByIdAcceptsOnlyNumericIds(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( 'Only numeric IDs are allowed.' ); findById("10 bananas"); }
Tests übertragen natürlich nichts. Aber jetzt wissen Sie, was zu tun ist, damit sie senden. Korrigieren Sie den Fehler, machen Sie die Tests grün, stellen Sie die Anwendung bereit und seien Sie zufrieden.
Behalten Sie diesen Test bei sich. Wann immer möglich, in einer Reihe von Tests, um Probleme mit der Regression zu lösen.
Das ist alles! Schnelles Feedback, Fehlerkorrekturen, Dokumentation, regressionsresistenter Code und Zufriedenheit.
Letztes Wort
Vieles davon ist nur meine persönliche Meinung, die ich während meiner Karriere entwickelt habe. Dies bedeutet nicht, dass der Rat wahr oder falsch ist, es ist nur eine Meinung.