Reine Tests in PHP und PHPUnit


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(); // assert it contains people } 

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.

 // extract the fetching // logic to a specialized // interface interface PeopleProvider { public function getPeople(): array; } // create a concrete implementation class JsonFilePeopleProvider implements PeopleProvider { private const PEOPLE_JSON = 'people.json'; public function getPeople(): array { $rawPeople = file_get_contents( self::PEOPLE_JSON ) ?? '[]'; return json_decode( $rawPeople, true ); } } class PeopleService { // inject via __construct() private PeopleProvider $peopleProvider; public function getPeople(): array { return $this->peopleProvider ->getPeople(); } } 

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 { // Given $person = $this->givenAPersonHasNoWings(); // Then $this->assertEquals( false, $person->canFly() ); // Further cases... } private function givenAPersonHasNoWings(): Person { return new Person(0); } 

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 :

 // ... $person = $this->givenAPersonHasNoWings(); $this->assertFalse( $person->canFly() ); // Further cases... 

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 { // ... $token $this->token = $token; } // @depends testGenerateJWTToken public function testExecuteAnAmazingFeature(): void { // Execute using $this->token } // @depends testExecuteAnAmazingFeature public function testStateIsBlah(): void { // Poll for state changes on // Logged-in interface } 

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 { // Given $token = $this->givenImAuthenticated(); // When $this->whenIExecuteMyAmazingFeature( $token ); $newState = $this->pollStateFromInterface( $token ); // Then $this->assertEquals( 'my-state', $newState ); } 

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(); // Early return if cookie // override is present if (Cookies::exists( $cookieName )) { return Cookies::get( $cookieName ); } // Evaluate feature toggle... } } 

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:

 // Test class... private Cookies $cookieMock; private FeatureToggle $service; // Preparing our service and dependencies public function setUp(): void { $this->cookieMock = $this->prophesize( Cookies::class ); $this->service = new FeatureToggle( $this->cookieMock->reveal() ); } public function testIsActiveIsOverriddenByCookies(): void { // Given $feature = $this->givenFeatureXExists(); // When $this->whenCookieOverridesFeatureWithTrue( $feature ); // Then $this->assertTrue( $this->service->isActive($feature) ); // additionally we can assert // no other methods were called } private function givenFeatureXExists(): Id { // ... return $feature; } private function whenCookieOverridesFeatureWithTrue( Id $feature ): void { $cookieName = $feature->getCookieName(); $this->cookieMock->exists($cookieName) ->shouldBeCalledOnce() ->willReturn(true); $this->cookieMock->get($cookieName) ->shouldBeCalledOnce() ->willReturn(true); } 

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:

 // class Person public function eatSlice(Pizza $pizza): void { // test exception if ([] === $pizza->slices()) { throw new LogicException('...'); } // test exception if (true === $this->isFull()) { throw new LogicException('...'); } // test default path (slices = 1) $slices = 1; // test alternative path (slices = 2) if (true === $this->isVeryHungry()) { $slices = 2; } $pizza->removeSlices($slices); } 

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.

Source: https://habr.com/ru/post/de485124/


All Articles