Dieser Blog-Beitrag soll jungen Entwicklern die Möglichkeit geben, sich schnell mit Unit-Tests vertraut zu machen. Es ist weder ein Ersatz für die Dokumentation, noch deckt es jeden Aspekt des manchmal komplexen und emotionalen Themas des Unit-Testings ab.
Es handelt sich um einen langen Beitrag mit rund 8000 Wörtern. Wenn du ihn nur liest, dann benötigst du möglicherweise eine Stunde. Wenn du die Beispiele durcharbeiten möchtest, dann brauchst du vermutlich zwei Stunden Zeit.
Alle PHPUnit-Codebeispiele findest du auf GitHub. Sende einen Pull-Request oder erstelle einen Issue, wenn du einen Fehler entdeckst oder weißt, wie man etwas besser machen könnte.
Vorschläge, Verbesserungen und sonstige Kommentare sind willkommen!
Du solltest wissen, wie man grundlegendes PHP schreibt (offensichtlich). Außerdem sollten dich Klassen und Objekte nicht verwirren.
Du solltest composer als Abhängigkeitsmanager verwenden. Traditionell wurden Abhängigkeitsmanager bei PHP recht spät eingeführt, weswegen einige sagen, dass es nicht notwendig ist, einen zu verwenden. Ich bin anderer Meinung: Du brauchst composer.
Wenn du noch nichts von composer gehört hast, empfehle ich dir, zuerst etwas über composer zu lernen und dann an diesen Ort zurückzukehren. Alle seriösen Frameworks können mit composer geladen werden.
Angenommen, du behebst einen Fehler in deinem Code. Die Zeit vergeht und Wochen später beschwert sich ein Kunde, dass genau dieser Fehler in einer der aktuellen Softwareversionen wieder eingeführt wurde. Das führt zu einer Verschwendung von Zeit, Energie und Geld. Du musst Dinge reparieren, die zuvor bereits repariert wurden. Dein Chef könnte dich für einen Idioten halten und deine Kunde könnte sich überlegen, zu einen anderen Anbieter zu wechseln.
Und es ist nicht einmal deine Schuld, könnte man meinen. Weil der Kunde immer diese verrückten Sachen will. Du hast sicherlich keine Zeit, um wirklich über die Dinge nachzudenken und bist möglicherweise immer an der Leistungsgrenze.
Kommt dir das bekannt vor? Wir haben nie genug Zeit. Warum noch mehr Zeit mit dem Schreiben von Unit-Tests verschwenden?
Das Schreiben eines Testfalls für deine PHP-Klasse kann so lange dauern wie das Schreiben der Zielklasse. Wenn du vier Stunden für das Schreiben der Klasse in Betracht ziehst (oder für das Hinzufügen einer neue Funktion), kannst du sicher weitere vier Stunden für den Test einrechnen. Manchmal geht es schneller, aber Testfälle können komplex werden. Wenn das der Fall ist, kann es viel Zeit in Anspruch nehmen. Auch das Testen von Datenströmen aus unzuverlässigen Quellen kann aufwendig sein. Das könnte dazu führen, dass du am Ende 100 Zeilen Testcode schreibst, nur um eine Zeile Quellcode zu testen.
Basierend auf der obigen Schätzung könnte man sagen, dass Unit-Tests die Kosten eines Features verdoppeln.
Andererseits: Wie viel Zeit und Geld würde man verlieren, wenn man einen Fehler zwei- oder dreimal beheben muss? Und was hätte es für Auswirkungen auf deinen Ruf, wenn du eine Version 1.0 einführst und der erste Klick eines Kunden einen unangenehmen Randfall auslöst, der nie in Betracht gezogen wurde?
Als selbständiger Entwickler kann ich nicht das Risiko eingehen, mehr Zeit als nötig in ein Feature zu investieren. Ich kann das Risiko nicht eingehen, dass die Leute denken, ich sei schlampig mit meiner Arbeit.
Wenn ich etwas manuell teste, kostet es mich außerdem auch Zeit. Ein PHPUnit-Test dauert nur wenige Sekunden. Wie lange dauert ein manueller Testaufbau? Ich denke, wenn man drei Versionen einer Software veröffentlicht, dann hat man bereits genug Zeit gespart, um den Komponententest zu bezahlen.
Und was ist mit der Aktualisierung der PHP-Version? Wenn die Testabdeckung sehr hoch ist, dann kann man fast sicher sein, dass auch mit einer neueren PHP-Version alles wie erwartet funktioniert.
Und wie kann man nach einem Refactoring sicherstellen, dass die öffentlichen Schnittstellen noch genau so funktionieren wie vor der Änderung?
Ich habe nicht die Zeit und das Geld, um KEINE Tests zu schreiben. Testfälle sparen tatsächlich Geld.
Stellen Dir vor, du schreibst für jedes Problem, das du beheben möchtest einen Testfall, der das jeweilig Szenario abdeckt. Wenn du deinen Test bei jedem Commit automatisch ausführst (das werde ich später behandeln), dann kannst du sicher sein, dass dieses Problem in Zukunft nicht mehr auftreten wird. Du würden es sofort bemerken, sobald die Tests laufen. Einige von ihnen schreiben dir sogar E-Mails, dass du sie nicht vergisst.
Es besteht außerdem immer die Möglichkeit neue Fehler einzuführen. Es ist besser, sich nicht mit Fehlern beschäftigen zu müssen, die bereits behoben wurden. Die neuen hinzukommenden sind genug. Natürlich könnte man vor dem Bildschirm sitzen und alles manuell testen. Aber die Leute langweilen sich von wiederholenden Aufgaben. Und auch das Testteam kann Fehler machen.
Unit-Tests gehen uns zwar manchmal auf die Nerven, aber sie lügen nicht und sind normalerweise im Recht.
Es ist fast unmöglich, eine Klasse von 1000 Zeilen mit einem Unit-Test zu testen. Das nennt man Spaghetti-Code, falls du davon noch nichts gehört hast und viele Leute schlagen vor, solche Klassen umzugestalten, damit sie kürzer sind und besser gelesen werden können. Eine Klasse erfüllt nur eine Zweck. Und eine Methode ist nur für eine Sache zuständig.
Wenn du weißt, dass eine Klasse wie diese nicht getestet werden kann, was wäre, wenn du einen Code anstreben würdst, der getestet werden könnte? Bereits beim Schreiben des Codes darf man das Testen nicht aus den Augen verlieren. Man sollte sich fragen: “Wie kann ich es testen, wenn meine Arbeit erledigt ist?”
Wie würde dein Code aussehen, wenn du so denken würdest? Es gibt viele Codierungsempfehlungen. Die Leute reden nicht darüber, weil sie wie Experten wirken wollen. Diese Empfehlungen helfen uns und manchmal kann man anhand der Unit-Tests nachvollziehen, wie gut die Klassen und Methoden entworfen wurden.
Es gibt Leute, die noch einen Schritt weiter gehen: Sie schreiben den Testfall, noch bevor sie mit ihrer eigentlichen Klasse beginnen. Der Zweck ist klar: Bevor man anfängt, wie ein Verrückter zu programmieren, muss man sich darüber im Klaren sein, was man tatsächlich erreichen will. Dieses Denken alleine verbessert bereits den Code. Und im Laufe der Zeit, mit der Erfahrung, wird man sich noch weiter verbessern.
Jede neue Funktion und jede Fehlerbehebung benötigt einen Test. Eine Ausnahme wäre so etwas wie ein Konfigurationsfehler. Diese Art von Fehler ist leicht zu erkennen, da sie bereits zum Startzeitpunkt auftreten sollten. Alles, was echte Geschäftslogik ist, sollte so oft wie möglich getestet werden.
So viele wie möglich :)
Beim Testen musst du das Richtige machen. Meine goldene Regel ist, mindestens 80% des Quellcodes zu testen. Damit bist du schon ziemlich sicher. Man sollten jedoch mehr anstreben, auch wenn man höchstwahrscheinlich nicht die 100% erreichen wird. Bevor man die Zeit mit dem Testen von Problemen verschwendet, die nur auf Servern mit beschädigtem Arbeitsspeicher auftreten, sollten man sich lieber mit Szenarien aus der Praxis befassen.
Die Kernklassen sollten sehr gut abgedeckt und unter verschiedenen Bedingungen behandelt werden. Es ist nützlich, nicht nur die Fälle abzudecken, die man erwartet oder die die “normale” Vorgehensweise betreffen.
Man sollte auch testen, was passieren würde, wenn der Benutzer Eingaben vergisst, die Start- und Endzeit vertauscht oder man aus unbekannten Gründen Null als Eingabe erhält. Vergiss nicht, wir befinden uns jetzt in einer JavaScript-Welt. Seltsame Dinge können passieren.
Verwende das richtige Werkzeug für die Aufgabe und befolge nicht jede Empfehlung blind, sondern mach dir deine eigenen Gedanken und triff deine eigenen Entscheidungen.
Ich mag TDD ziemlich gern, aber ich benutze es nicht die ganze Zeit, um ehrlich zu sein. Das ist nicht unbedingt schlecht. Manchmal ist es einfacher, nicht mit einem Test zu beginnen, sondern den Test anschließend zu schreiben. Mit etwas Erfahrung wirst du die richtigen Entscheidungen treffen. Ich empfehle dir, zuerst mit TDD zu beginnen, damit du selbst sehen kannst, welche Vorteile es dir bringt. Sobald man die Regeln kennt, kann man anfangen, sie zu brechen, je nachdem wie man es für richtig hält.
Außerdem gibt es einige Randfälle, die ich nicht teste. Einige Leute würden mir hier nicht zustimmen, aber ich bin pragmatisch. Ich benutze mein eigenes Gehirn, um zu entscheiden, was abgedeckt werden muss und was nicht. Versteht mich nicht falsch: Faulheit oder das “Keine Zeit”-Syndrom spielen hier keine Rolle.
“Das wird nie passieren” - hast du das schon als Quelltextkommentar gelesen? Wenn das Testen keinen Sinn ergibt, teste es nicht. In der Java-Welt gibt es viele Ausnahmen. Manchmal kommen sie vom Server. Was ist beispielsweise, wenn du sowieso nicht in einen Ausgabestream schreiben kannst? Die gesamte Anwendung würde ohnehin nicht mehr funktionieren, es ist also nicht erforderlich, dieses Problem zu testen. Mach das Richtige. Sei aber nicht faul und tu so, als wäre es das Richtige!
Du sollten keine Funktionen testen, die von einem Framework eines Drittanbieters stammen. Stattdessen solltest du die Abhängigkeiten mit Bedacht auswählen. Ich überprüfe immer, ob der Code von Drittanbietern bereits gut mit Testfällen abgedeckt ist. Wenn das nicht der Fall ist, suche ich etwas anderes, trage zum Projekt bei oder schreibe es selbst. Aber wiederhole keine Tests, die bereits von anderen durchgeführt wurden, es sei denn, du hast gute Gründe, dies zu tun.
PHPUnit ist ein Testframework. Es hilft dabei, Teile des Codes auszuführen und das Ergebnis zu ermitteln. Zum Beispiel kann man eine PHPUnit-Klasse schreiben (die als sogenannter “Testfall” bezeichnet wird), die eine Instanz eines internationalen Portorechners erstellt und sicherstellt, dass die Berechnungen für jedes Land korrekt sind.
PHPUnit bietet eine ausführbare Datei zum Durchführen der Tests, eine Möglichkeit zum Organisieren der Tests und Methoden zum Überprüfen der Ergebnisses. Was es nicht kann, ist, deinen Code sauber und prüfbar zu machen.
In PHPUnit steckt viel mehr, als ich in einem einzigen Blogbeitrag beschreiben kann. Es lohnt sich, die PHPUnit-Dokumentation zu lesen, sobald du die grundlegenden Schritte zum Testen von Einheiten beherrschst.
Historischer Hinweis: PHPUnit ist eigentlich eine Portierung des beliebten JUnit-Projekts, das von einigen engagierten Java-Programmierern geschrieben wurde.
In der PHPUnit-Dokumentation wird vorgeschlagen, es manuell herunterzuladen und global zu installieren. Je nachdem, an wie vielen Projekten du parallel arbeitest, ist das möglicherweise in Ordnung. Ich habe jedoch mit vielen verschiedenen Projekten zu kämpfen, zwischen denen ich oft wechseln muss und bei denen unterschiedliche PHPUnit-Versionen verwendet werden.
Glücklicherweise hat der Erfinder von PHPUnit seine Arbeit auch zu Packagist hochgeladen, dem zentralen Repository von Composer.
Öffne das Terminal oder die Shell im Projektordner und gib folgendes ein:
$> composer require phpunit/phpunit --dev
Dadurch wird PHPUnit zu den development requirements hinzugefügt. PHPUnit sollte jedoch nicht an die Produktionsumgebung weitergegeben werden, es reicht aus, es in der Entwicklungsumgebung zu verwenden.
Du kannst überprüfen, ob das Ausführen der Binärdatei aus dem Vendor-Ordner von composer funktioniert:
$> vendor/bin/phpunit --version
PHPUnit 5.7.2 by Sebastian Bergmann and contributors.
Einige IDEs wie PHPStorm unterstützen das sofortige Ausführen von PHPUnit. Das ist ziemlich komfortabel, weil man den Testfall einfach mit der Maus auswählen und per Mausklick ausführen kann.
Bereit? Beginnen wir mit unserem ersten Testfall.
Ok, jetzt wird es ernst. Hier ist mein typisches Composer-Projektlayout:
├── src
│ ├── Application.php
│ ├── ...
├── tests
│ ├── assets
│ ├── bootstrap.php
│ ├── ApplicationTest.php
│ └── ...
Du siehst, ich habe den Testcode vom Quellcode getrennt. Ich weiß nicht, wie du deinen Code auf deinen Servern bereitstellst, aber in der Regel wird der Testcode nicht auf den Server hochgeladen. Man testen und wenn man zufrieden ist, wird nur das veröffentlicht, was für die Ausführung der Anwendung unbedingt erforderlich ist. Dieser Code befindet sich normalerweise im Ordner src.
In meinem Testordner habe ich einen Assets-Ordner. Ich verwende ihn, um bestimmte Ressourcen zu speichern, die ich testen muss. Beispielsweise möchtest du einige Daten testen, die über eine API eingehen. Dann könntest du hier eine JSON-Datei ablegen. Oder du benötigst einige Bilder zu Testzwecken, dann ist der Assets-Ordner der richtige Ort. Der Assets-Ordner ist meine eigene Erfindung. Andere Leute könnten andere Namen, Orte oder was auch immer dafür haben. In der Java-Welt z. B. wird es oft als “src/tests/resources”-Verzeichnis bezeichnet.
Ich habe meine Testressourcen in meinem Testordner, sodass ich alles an einem Ort habe.
Hast du die Datei bootstrap.php gesehen? Sie ist mehr oder weniger optional. Du kannst sie zur zusätzlichen Konfiguration verwenden. Für den Moment kannst du die Datei weglassen und die Standardeinstellungen verwenden.
Aber… mit Composer musst du möglicherweise einige Klassen laden. Du solltest folgenden Code hinzufügen:
require __DIR__ . '/../vendor/autoload.php';
Composer kann Abhängigkeiten und Klassen laden. Das befindet sich alles in der Datei autoload.php. Ohne Autoload funktioniert es nicht. Deshalb habe ich “mehr oder weniger” optional gesagt. In vielen Fällen wirst du es brauchen.
Eine andere Idee besteht darin, eine Konstante zu definieren:
if (!defined("TEST_ROOT")) {
define("TEST_ROOT", realpath(__DIR__));
}
Oder ein etwas fortgeschrittenerer Code, wie das Implementieren einer notwendigen Funktion zum Starten eines Frameworks:
function get_test_app()
{
$env = new Environment(Environment::TEST);
$app = new Application($env);
...
Oder man konfiguriert log4php.
Auf jeden Fall sollte die Bootstrap-Datei so kurz wie möglich sein.
Du kannst auch mehrere Bootstrap-Dateien haben und diese beim Starten von PHPUnit auswählen. Das würde so aussehen:
$> vendor/bin/phpunit --bootstrap ./tests/bootstrap.php MyTestClass.php
Mit der Bootstrap-Option kann man entscheiden, welche Datei man verwendet und damit die Umgebung und den Kontext spezifizieren.
Ich habe eine einfache Klasse geschrieben, die so aussieht:
namespace Grobmeier\PHPUnit;
class Simple
{
private $number;
public function __construct($number)
{
$this->number = $number;
}
public function divide($divisor)
{
if (empty($divisor)) {
throw new \InvalidArgumentException("Divisor must be a number");
}
return $this->number / $divisor;
}
}
Siehe GitHub.
Wie du siehest, ist diese Klasse wirklich keine große Sache. Als Konstruktorargument wird eine Zahl verwendet, die als Membervariable gespeichert wird. Die Methode divide verwendet ein weiteres Argument, das als Teiler für die Membervariable verwendet wird.
Wenn der Divisor leer ist, wir eine Ausnahme ausgelöst. Leer bedeutet in PHP, dass es 0, null, false und so ähnliches ist. Ich hätte explizit 0 schreiben können, aber ich mag die empty()-Methode lieber.
Jetzt kommt hier der interessantere Teil. Erstelle im Ordner tests eine weitere PHP-Klasse und nenne sie genau so wie die Klasse, die du testen möchtest. Füge dazu am Ende das Klassenamens einfach den Begriff “Test” hinzu.
PHPUnit identifiziert den Testfall als Test, da er das Suffix “Test” besitzt Wenn die Testklasse wie das Testziel benannt wird, dann behält man den Überblick. Ausnahmen sind zulässig, ich empfehle jedoch, den Namen der ursprünglichen Klasse so weit wie möglich beizubehalten.
Darüber hinaus wird empfohlen, in der Testklasse denselben Namespace zu verwenden den man in der Zielklasse verwendet. Es ist vielleicht keine große Sache mit PHP, aber es hilft dir, dich gut organisiert zu halten. Beide Klassen bleiben im selben Ordner und du musst nie danach suchen. Zweitens gibt es in einigen Sprachen wie Java einen “package scope” von Methoden. PHP kennt das nicht, aber es könnte eines Tages eingeführt werden (ich hoffe sie tun es!). Wenn es keine guten Gründe gibt, vom Namespace abzuweichen, sollten man ihn beibehalten.
Schauen wir uns unsere Testklasse an:
namespace Grobmeier\PHPUnit;
class SimpleTest extends \PHPUnit_Framework_TestCase
{
public function testDivide()
{
$simple = new Simple(10);
$result = $simple->divide(2);
$this->assertEquals(5, $result);
}
}
Siehe GitHub.
Das erste, was dir auffallen wird ist, dass alle unsere Testklassen von PHPUnit_Framework_TestCase erben.
Wenn du gerade erst mit PHP begonnen hast, könnte dir der seltsame Namen PHPUnit_Framework_TestCase seltsam vorkommen. Das hat historische Gründe. PHPUnit ist ein ausgereiftes Projekt und vor nicht allzu langer Zeit kannte PHP noch keine Namespaces. Die Leute haben damals ihre eigenen Namespaces mit langen Namen und vielen Unterstrichen erstellt. Diese Zeiten sind vorbei, aber das Framework PHPUnit besteht wie gesagt schon länger.
Bitte beachte: Es werden nur Klassen ausgeführt, die von PHPUnit_Framework_TestCase erben.
Eine andere Namenskonvention: Alle Methoden, die in deiner Testklasse vorkommen und die von PHPUnit ausgeführt werden sollen, müssen mit dem Begriff “test” beginnen. Auf den ersten Blick könnte das hässlich aussehen. Wenn die Tests jedoch umfangreicher und komplexer werden, möchten man die Möglichkeit haben, eigenen Methoden zu definieren, die von PHPUnit nicht direkt berührt werden.
Und wenn du wirklich nach Best Practice arbeiten möchtest, solltest du die Testmethode wie die zu testende Methode nennen. Aber in der Praxis fand ich das nicht immer praktikabel. Ich versuche, den Methodennamen etwas aussagekräftiger zu gestalten
Um die PHPUnit-Namenskonventionen zusammenzufassen:
Was passiert innerhalb der Testmethode?
Wir erstellen ein Objekt, führen eine Methode darauf aus und überprüfen schließlich das Ergebnis mit der assertEquals-Methode. “assertEquals” wird von PHPUnit_Framework_TestCase bereitgestellt. Das erste Argument, das angeben wird, ist das, was man erwarten würden. Das zweite Argument ist das tatsächliche Ergebnis der Klasse.
Es gibt viele verschiedene Assertion-Methoden in PHPUnit. Im täglichen Leben brauchst du wahrscheinlich nur einige davon, wie assertEquals, aber es lohnt sich, sie von Zeit zu Zeit in der offiziellen Dokumentation nachzuschlagen, da du dir damit viel Zeit sparen kannst.
Zeit zum Ausführen! Öffne deine Kommandozeile und mach folgendes:
$> vendor/bin/phpunit tests
PHPUnit 5.7.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 37 ms, Memory: 3.50MB
OK (1 test, 1 assertion)
Ich führe das in meinem Projektstammordner aus. Dadurch wird die installierte Binärdatei von Composer in dem Ordner ausgeführt, in dem sich meine Tests befinden. Der Vollständigkeit halber habe ich im Ordner tests noch eine bootstrap.php angelegt. Sie wird standardmäßig mit ausgewählt.
Gibt es Probleme? Wenn du gerade dein Projekt einrichtest und auf Probleme stößt,
dann versuche, composer install
auszuführen.
Manchmal fehlt nur die Autoloader-Datei oder ist nicht up-to-date.
In der obigen Ausgabe sieht man, dass ein Test (SimpleTest) mit einer Assertion (Zusicherung) ausgeführt wurde (das ist der Aufruf “$this->assertEquals”). Alles verlief erfolgreich.
Lass uns einen Gegencheck machen. Wenn ich den assertEquals-Parameter so ändere, dass der nicht mehr korrekt ist, dann sieht ein PHPUnit-Testdurchlauf folgendermaßen aus:
vendor/bin/phpunit tests
PHPUnit 5.7.2 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
Time: 36 ms, Memory: 3.75MB
There was 1 failure:
1) Grobmeier\PHPUnit\SimpleTest::testDivide
Failed asserting that 5 matches expected 7.
/phpunit-examples.git/tests/SimpleTest.php:11
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
PHPUnit war so nett, uns mitzuteilen, dass ein Test fehlgeschlagen ist. Ein Misserfolg bedeutet, wir haben eine Assertion, die nicht das zurückbrachte, was wir erwarten hatten. Außerdem erfahren wir genau, in welcher Zeile die Assertion gebrochen wurde. Das ist ziemlich cool, um unseren Code zu debuggen.
PHPUnit unterscheidet außerdem zwischen Failures und Errors. Ein Failure bedeutet, dass die Vorgänge normal abgeschlossen wurden, das Ergebnis jedoch unerwartet ist. Error hingegen bedeutet, dass etwas schief gelaufen ist und es Probleme gibt.
Stell dir vor, wir hätten den folgenden Code:
public function divide($divisor)
{
throw new \Exception();
}
Offensichtlich funktioniert das nicht so gut für unser Projekt. Das Ergebnis in PHPUnit würde so aussehen:
vendor/bin/phpunit tests
PHPUnit 5.7.2 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 29 ms, Memory: 3.25MB
There was 1 error:
1) Grobmeier\PHPUnit\SimpleTest::testDivide
Exception:
/phpunit-examples.git/src/Simple.php:19
/phpunit-examples.git/tests/SimpleTest.php:9
ERRORS!
Tests: 1, Assertions: 0, Errors: 1.
In beiden Fällen muss man Nachforschungen anstellen. Aber es lohnt sich den Unterschied zu kennen.
Wir wissen jetzt, dass unsere Klasse funktioniert, wenn wir die richtigen Argumente verwenden. In unserem Code haben wir jedoch folgendes explizit erwähnt:
public function divide($divisor)
{
if (empty($divisor)) {
throw new \InvalidArgumentException("Divisor must be a number");
}
...
Wenn der Divisor leer ist, möchten wir eine Ausnahme mit einer bestimmten Nachricht auslösen und uns nicht darauf verlassen, was die Sprache in diesem Ausnahmefall macht. Wir erwarten eine Ausnahme und wenn du in Zukunft gut schlafen möchtest, müssen wir auch diesen Ausnahmefall testen.
Wie teste ich Ausnahmen (Exceptions)?
Wir verlassen jetzt die üblichen PHP-Pfade, da PHPUnit sogenannte Annotationen verwendet. Das Problem ist, dass Annotationen (leider) nicht von der PHP-Programmiersprache unterstützt werden. PHPUnit behebt dieses Problem durch die Verwendung von Annotationen in den PHP-Dokumenten (als Kommentar). Eigentlich gibt es auch einen programmatische Lösung dafür, aber die Annotationen sind in diesem Fall gut geeignet.
Puristen finden das vielleicht unsauber. Normalerweise würde ich zustimmen. Annotationen sollten nativ sein und keine Kommentare. Aber PHPUnit ist ein bisschen anders. Es testet Code und ich möchte nur Code in meiner Methode überprüfen, sonst nichts.
Testcode ist nicht unbedingt schön oder elegant, er sollte lediglich Druck auf die Muttern und Schrauben ausüben, die unsere Anwendungen am Laufen halten.
So sieht es aus:
/**
* @expectedException \InvalidArgumentException
*/
public function testDivideWithException()
{
$simple = new Simple(10);
$simple->divide(0);
}
Wir verwenden hier die Annotation @exepectedException. PHPUnit erkennt, dass wir hier eine Ausnahme mit dem Typ InvalidArgumentException erwarten. Wenn diese nicht ausgelöst wird, behandelt PHPUnit es als einen Fehler.
Wir wissen, wie dieser Test aussieht, wenn er bestanden wird. Wenn er jedoch fehlschlägt, erhalten wir folgende Ausgabe:
2) Grobmeier\PHPUnit\SimpleTest::testDivideWithException
Failed asserting that exception of type "\InvalidArgumentException" is thrown.
Ganz ordentlich, nicht wahr?
Wenn du mit der Verwendung einer Annotation WIRKLICH sehr unzufrieden bist, könntest du folgendes auch direkt in den Testfall schreiben:
public function testException()
{
$this->expectException(InvalidArgumentException::class);
// ... test code
}
Wenn du noch einen Schritt weiter gehen möchtest, kannst du in der Dokumentation nachlesen, wie man Ausnahmecodes und Ausnahmemeldungen testen kann.
Alles in allem ist diese Art von Test schon ziemlich gut. In PHPUnit warten aber noch weitere Annotation auf dich, die je nach Projekt mehr oder weniger hilfreich sein können.
Wie bereits erwähnt, sind Assertions das Einzige, was du in- und auswendig kennen solltest. Das haben wir schon gelernt:
$this->assertEquals($expected, $actual);
Dieser Code testet auf einen einzelnen Wert.
Aber es gibt noch mehr. Viel mehr, als ich in einem einzigen Blogbeitrag behandeln könnte. Es können Dateien, Arrays, XML usw. überprüft werden. Hier sind einige, die ich häufig benutze.
$this->assertNull($assertions->getNull());
Du kannst assertNull verwenden, um Nullwerte zu überprüfen. Oder assertNotNull, um Nicht-Null-Werte zu überprüfen. Das ist kürzer als assertEquals (null, $ value). Und besser für deine Augen (zumindest meine).
$this->assertTrue($assertions->getTrue());
Natürlich gibt es auch ein Gegenstück zur Überprüfung auf falsche Werte.
Auch das Überprüfen von Arrays ist interessant. Nehmen wir an, die Methode getFruits sieht folgendermaßen aus:
public function getFruits()
{
return [
'peach' => 'sweet',
'melon' => 'watery',
'apple' => 'sour',
'banana' => 'amazing'
];
}
Und jetzt wollen wir sicherstellen, dass mindestens ein Apfel da ist! Ich mag Bananen mehr, aber manchmal sind mir Äpfel lieber, besonders wenn ich versuche abzunehmen :-( Dieser Code überprüft, ob es einen bestimmten Schlüssel im Array gibt:
$this->assertArrayHasKey('apple', $assertions->getFruits());
Wertvoll, aber nicht sehr präzise. Wir können auch überprüfen, ob das Ergebnisses eine bestimmte Teilmenge enthält:
$this->assertArraySubset( [
'melon' => 'watery',
'apple' => 'sour'
], $assertions->getFruits());
Nett. Wir haben jedoch vier Früchte, nicht nur zwei. Vielleicht wissen wir nicht welche, aber wir müssen sicherstellen, dass es immer vier sind. assertCount hilft:
$this->assertCount(4, $assertions->getFruits());
Super. Aber wenn du es wirklich “sicher” machen willst, dann musst du natürlich alles vergleichen. Sehen wir uns also unseren alten Freund “assertEquals” nochmal an. Wir könnten das so lösen:
$this->assertEquals([
'apple' => 'sour',
'peach' => 'sweet',
'melon' => 'watery',
'banana' => 'amazing'
], $assertions->getFruits());
Bitte beachte, dass die oben erwartete Reihenfolge von der Reihenfolge der getFruits-Methode abweicht. In diesem Fall funktioniert es gut, aber abhängig von jeweiligen Fall muss man die Parameter ggf. zuerst mit sort() sortieren.
Das ist großartig. Für das Frontend benötigen wir heute JSON. Lass uns überprüfen, ob unser generierter JSON-String so aussieht, wie wir ihn erwarten.
$this->assertJson('{"peach":"sweet","melon":"watery","apple":"sour","banana":"amazing"}', $assertions->getJsonFruits());
Jetzt haben wir bereits ein großartiges Toolset in der Hand, um viele Tests zu schreiben. Herzlichen Glückwunsch, dass du es so weit geschafft hast. Nun beginnen wir mit den fortgeschritteneren Tests.
Mit dem, was wir bis jetzt gelernt haben ist PHPUnit bereits ein sehr mächtiges Werkzeug. Wenn du weiterliest, lernst du, wie das Testen noch komfortabler werden kann und wie man gut organisierte Tests aufbaut.
Angenommen, wir haben eine Liste von Objekten, an denen wir mehrere Tests durchführen möchten. Es wäre möglich, diese Liste in jedem einzelnen Testfall zu erstellen. Aber das Prinzip “don’t repeat yourself” besagt, dass wir es besser machen sollten. Die nächste Idee wäre, eine separate Methode zu schreiben, die alles konstruiert, was wir brauchen. Das ist eigentlich viel besser, aber wir müssten diese Konstruktionsmethode in jeder einzelnen Testmethode aufrufen. Einige Leute könnten sagen, das sei unnötig. Darüber hinaus ist die Konstruktion der Testdaten nicht Bestandteil des eigentlichen Tests und sollte daher nicht im Code enthalten sein.
Wenn du auch so denkst, wirst du setUp und tearDown lieben. Wenn wir zwei Methoden mit diesem Namen implementieren, behandelt PHPUnit diese speziell. Die setUp()-Methode wird aufgerufen, noch bevor JEDE andere Testmethode ausgeführt wird und tearDown() wird aufgerufen, NACHDEM die Testmethode ausgeführt wurde.
Schau dir diesen Code an, der die Verwendung von setUp hervorhebt:
private $names;
public function setUp()
{
$this->names = [
'Christian',
'Nicole',
'Ben'
];
}
public function testMethod1()
{
$this->assertCount(3, $this->names);
$this->names[] = 'Surprise!';
$this->assertCount(4, $this->names);
}
public function testMethod2()
{
$this->assertCount(3, $this->names);
array_splice($this->names, 1, 1);
$this->assertCount(2, $this->names);
}
In diesem Testfall arbeiten wir mit einer Art Zustand. Dabei handelt es sich um die Variable $names. Bitte beachte, dass Zustände immer ein bisschen gefährlich sind und man von Fall zu Fall entscheiden muss, ob man sie wirklich braucht. In Bezug auf Testfälle würde ich sicherstellen, dass nur eine Testklasse zum Testen eines bestimmten Falls existiert und nur so wenig Zustände wie nötig vorhanden sind. Man darf bei verschiedenen Zustände innerhalb eines Testfalls nicht durcheinander kommen.
Abgesehen von dieser Warnung können wir jetzt die $names-Liste für jeden Testmethodenaufruf erstellen. Dies geschieht mit der Methode setUp. Bevor etwas passiert, stellt setUp sicher, dass die Liste drei Namen enthält.
Wir können das bereits vor dem Ausführen überprüfen, was aber eigentlich dem Testprinzip widerspricht, das besagt, nicht das zu testen, was man nicht selbst geschrieben hat (es sei denn, man programmiert möglicherweise medizinische Geräte).
In Testmethode 1 sehen wir, dass es drei Namen gibt, dann fügen wir einen Namen hinzu und überprüfen, ob das Hinzufügen funktioniert hat. In Testmethode 2 wird behauptet, dass es wieder nur drei Namen gibt. Was wiederum bedeuten würde, dass die setUp-Methode wieder den Ursprungszustand hergestellt hat, was sie auch getan hat.
setUp() ist sehr hilfreich. tearDown ist auch nett, wird aber meistens nicht so oft wie setUp verwendet. Es ist das Gegenstück zu setUp und ist für das Aufräumen von “Dingen”, wie z. B. Ressourcen, zuständig.
Man kann beispielsweise Ressource in setUp() öffnen und in tearDown() wieder schließen.
private $resource = false;
public function setUp()
{
...
if (!$this->resource) {
$this->resource = true;
print_r("Resource opened" . PHP_EOL);
}
}
public function tearDown()
{
if ($this->resource) {
$this->resource = false;
print_r("Resource closed" . PHP_EOL);
}
}
Siehe GitHub.
Bevor ich das erkläre, noch eine Bemerkung zu guten Testfällen: Man sollte in den Testfällen nichts in die Konsole schreiben. Etwas in die Konsole zu schreiben ist normalerweise ein Zeichen dafür, dass ein Entwickler zu faul ist, um Assertions zu erstellen! “Manuelle Assertions” durch Lesen der Konsolenausgabe widersprechen der Idee des automatischen Testens. Mach das also nicht. Ich habe es hier nur zur Veranschaulichung gemacht, damit man sehen kann, wann PHPUnit was aufruft.
Folgendes sehe ich, wenn ich meinen Testfall durchführe:
PHPUnit 5.7.2 by Sebastian Bergmann and contributors.
Resource opened
Resource closed
Resource opened
Resource closed
Time: 24 ms, Memory: 3.50MB
Der Code öffnet vor jedem Testaufruf eine Ressource, falls diese noch nicht geöffnet ist. Und sie wird geschlossen, sobald die Testmethode ausgeführt wurde. Du muss entscheiden, ob das im speziellen Fall effizient ist oder nicht und ob du ggf. zu anderen Ideen übergehen solltest.
Es gibt noch eine Sache, die gut zu wissen ist. Es gibt zwei Methoden, ähnlich wie setUp und tearDown, die genau dasselbe tun, jedoch auf Klassenebene.
Die Methoden heißen setUpBeforeClass () und tearDownAfterClass (). setUpBeforeClass wird ausgeführt, bevor die erste Testmethode in einer Testklasse ausgeführt wird und tearDownAfterClass wird aufgerufen, nachdem die letzte Testmethode ausgeführt wurde. In einigen Fällen ist dies möglicherweise der bessere Ort, um Ressourcen zu öffnen oder zu schließen.
Testfälle sollte so atomar und klein wie möglich gestaltet werden, da sie von Zeit zu Zeit von allein komplexer werden.
Außerdem sollten die Testmethoden und Testfälle so geschrieben werden, dass sie unabhängig voneinander sind. Jeder Testfall sollte genau eine Sache testen und in der Lage sein, unabhängig von allen anderen zu laufen. setUpBeforeClass () wird oft missbraucht, um einen einzigen Datenpool zu erstellen, der von vielen Testmethoden bearbeitet wird. Es kommt dann oft vor, dass man nicht genau sehen kann, welche Testfälle fehlschlagen, denn sobald der erste fehlschlägt, kommt alles durcheinander. So kann man wichtige Hinweise übersehen die für die Problemebehebungen entscheidend sind.
Das Vorbereiten der Testdaten vor einem Testlauf wird als Schreiben eines “Fixtures” bezeichnet.
Wenn setUp() unübersichtlich und unlesbar wird, solltest du bei PHPUnits DataProviders nachschlagen. Mit Data-Providern kann man ganz einfach große Datenfelder definieren, auf die der Testfall dann angewendet wird.
Herzlichen Glückwunsch, dass du so weit gekommen bist. Du beherrscht jetzt die Grundlagen von PHPUnit und von nun an solltest du in der Lage sein, selbst einige verrückte Sachen zu schreiben und bei Bedarf die Dokumentation zu lesen. Unit-Tests umfassen jedoch nicht nur PHPUnit. Beginnend mit Mocks treten wir in die erweiterten Tests ein. Die folgenden Abschnitten werden dich befähigen, das Beste aus deinen Tests zu machen.
Wir beginnen mit dem Schreiben von Mocks.
In einem realen Szenario müssen wir uns manchmal mit einigen komplexen Objekten auseinandersetzen. Einige von ihnen sind sehr schwer zu erstellen, andere nehmen viel Zeit in Anspruch. Auch wenn wir sie möglicherweise testen müssen, können wir sie nicht jedes mal wieder neu erstellen. In einigen Fällen ist es auch einfach nicht möglich, die Objekte ordnungsgemäß zu erstellen, da sie von Drittanbieter kommen.
Damals “mock”-ten Entwickler diese Dienste von Drittanbietern manuell. Sie erstellten hier und da einige Schnittstellen und implementierten Code, um irgendwie so zu agieren, wie sie glaubten, dass der Drittanbieter agieren würde. Sozusagen ein Dienst für einen Dienst. Viel Arbeit.
Glücklicherweise können wir uns mit dem Framework Mockery einiges an Arbeit sparen. Es soll zusammen mit PHPUnit verwendet werden und bietet durch seine hervorragende Dokumentation viele gute Ansätze.
Füge dem require-dev-Teil der composer.json folgendes hinzu:
"mockery/mockery": "dev-master"
Verwende anschließend “composer install”, um die Abhängigkeiten zu aktualisieren. Zumindest schlagen die Mockery-Leute das so vor. Mit der Vorgabe, dev-master als Version zu verwenden, versprechen sie auch, ihren Zweig sehr stabil zu halten. Ich vertraue niemandem, wenn es um meine Abhängigkeiten geht, deshalb ziehe ich es vor, die Version selbst festzulegen.
Meine composer-Abhängigkeiten für diesen Blog-Beitrag sehen folgendermaßen aus:
"require-dev": {
"phpunit/phpunit": "^5.7",
"mockery/mockery": "0.9.6"
},
Entscheide selbst, ob du die Versionen wie ich festsetzen willst oder aber darauf vertraust, dass die Mockery-Leute nichts kaputtmachen (Hinweis: Ich könnte paranoid sein).
Beginnen wir nun mit einem einfachen Beispiel. Angenommen, wir möchten diese Serviceklasse verwenden:
class SimpleService
{
/** @var Simple $simple */
private $simple;
public function __construct(Simple $simple)
{
$this->simple = $simple;
}
public function dividingService($arg)
{
return $this->simple->divide($arg);
}
}
Siehe GitHub.
Die Simple-Klasse wurde bereits in diesem Blogeintrag definiert. Es wird nur eine Methode bereitgestellt, die den Konstruktorparameter durch den Methodenparameter teilt.
Wenn wir vermeiden möchten, die eigentliche Simple-Klasse zu verwenden, weil diese einen Webdienstes aufruft, müssen wir sie mocken. Zum Glück können wir einfach die Abhängigkeit von außen injezieren (en).
Folgendes würden wir in unserem Testfall schreiben:
public function testDivide()
{
$mock = \Mockery::mock(Simple::class);
$mock->shouldReceive('divide')->andReturn(5);
$service = new SimpleService($mock);
$result = $service->dividingService(10);
$this->assertEquals(5, $result);
}
Siehe GitHub.
Zuerst bitten wir Mockery einen neuen Mock zu erstellen, der auf der Simple-Klasse basiert. Was passiert ist, dass Mockery ein Objekt mit (fast) der gleichen Schnittstelle wie die unsere ursprüngliche Klasse erstellt. Die Methoden machen noch nichts Sinnvolles, nur die Methodensignaturen sind vorhanden.
In der zweiten Zeile teilen wir Mockery mit, was wir erwarten und was es tun soll. Das Simple-Objekt sollte einen Aufruf der Divisionsmethode erhalten. In unserem Fall soll Mockery eine 5 zurückgeben.
Der Rest ist einfacher Testcode. Wir erstellen den Service und fügen unseren Mock ein, rufen die Service-Methoden auf und vergleichen das Ergebnis.
Während dieses Beispiel in der Praxis keine Sinn ergibt (jeder würde die Simple-Klasse direkt erstellen), ist es in anderen Fällen sehr nützlich. Zum Beispiel kann man DAO-Klassen so mocken, dass sie für die Testfälle keine Datenbank benötigen.
Aber auch davon abgesehen ist es sehr nützlich, Fremdanbieterdienst so testen zu können. Aber bitte denk immer daran, dass wir nur Mocks und nicht das Original getestet haben. Trotzdem ist es sehr hilfreich einige Logiken in Serviceklassen testen zu können.
Mockery ist mächtig und obwohl ich hier nicht alles im Detail erklären möchte, möchte ich noch ein paar Dinge zeigen.
Angenommen, deine Serviceklasse würde einige Berechnungen durchführen und dann etwas in einer Datenbank speichern. Sicher möchtest du wissen, was dort gespeichert werden könnte. Wenn du ein DAO verwendest, dann bist du auf der sicheren Seite (falls du es nicht weißt: Ein DAO ist im Grunde eine spezialisierte Klasse, die nichts anderes tut, als deine Daten aus einer Datenbank zu lesen und zu bearbeiten. Such nach “Data Access Object”).
$mock
->shouldReceive('addPerson')
->once()
->withArgs(function($person) {
$this->assertTrue(Mockery::type(Person::class)->match($person));
$this->assertEquals('Grobmeier', $listing->name);
return true;
})
->andReturn(true);
Im obigen Beispiel wird überprüft, ob eine Person in der Datenbank gespeichert ist. Der Mock sollte einen Aufruf der “addPerson” -Methode erhalten. Hier ist die erste Änderung: Es sollte nur einmal ausgelöst werden. Andernfalls würden wir die Person doppelt speichern, was natürlich falsch ist. Die Methoden twice() oder times() (für mehrere Aufrufe) sind ebenfalls verfügbar.
Jetzt können wir das Argument mit der “withArgs” -Methode überprüfen. Diese Methode nimmt einen Callback entgegen und fügt die angegebenen Argumente ein. Jetzt, da ich Zugriff auf das Argument selbst habe, kann ich so viele Tests schreiben, wie ich will!
Die erste Assertion überprüft ob das Argument $person tatsächlich vom Typ Person ist. Du kannst dies auch mit einfachem PHP tun, aber Mockery hat diese netten kleinen Hilfs-Methoden zur Hand, die alles besser lesbar machen. Dann überprüfe ich den Namen (langweilig) und gebe true zurück, wenn meine Tests erfolgreich waren. Der letzte Aufruf besteht darin, Mockery mitzuteilen, was die Methode “addPerson” nach dem Aufruf zurückgeben soll.
Bei dem gesamten Test geht es um das Testen der Serviceklasse. Natürlich können wir nichts über die Datenbankklasse sagen, aber zumindest wird der Service wie erwartet funktionieren.
Ich benutze meistens ein kleines nettes Datenbank-Framework namens Phormium. Es ist minimalistisch und deshalb mag ich es. Irgendwann musste ich die Verbindung mocken und die tatsächlichen PDO-Aufrufe des Frameworks testen.
Bitte beachte, dass das Mocken der gesamten Datenbank heute möglicherweise nicht die beste Lösung ist. Heutzutage haben wir viele Möglichkeiten eine echte Datenbank zu testen, so dass wir es möglicherweise vermeiden können, hier zu mocken. Manchmal ist das Mocken jedoch schneller, besser und stabiler.
Zuerst musste ich die Connection-Klasse mocken, die die PDO-Ebene in Phormium umschließt:
$mockConn = \Mockery::mock(Connection::class);
$mockConn->shouldReceive('inTransaction')
->once()
->andReturn(false);
Ich habe auch sichergestellt, dass die Methode “inTransaction” false zurückgibt. Es gibt einige Phormium-Interna, die das überprüfen.
Dann musste ich testen, ob eine Auswahl getroffen wurde (oder nicht):
$person = new Person('Christian');
$person->weight = 80; // this is the weight I work hard for! Can't lie to you!
$mockConn->shouldReceive('preparedQuery')->once()
->with('/^SELECT /', [$id], \PDO::FETCH_CLASS, Person::class)
->andReturn([$person]);
Ein Aufruf von Phormiums Connection::prepareQuery prüft, ob der SQL-Code mit “SELECT” beginnt und welche Parameter, PDO-Abrufklassen und welche Klassen ich abrufen möchte. Wenn dies aufgerufen wird, gibt Mockery die zuvor erstellte Person zurück.
Mit Mockery können viele Dinge erledigt werden. Aber es gibt ein paar Dinge, die Mocking schwer oder sogar unmöglich machen können.
Zum Beispiel können statische Methoden nicht gemockt werden. Eine statische Methode ist immer vorhanden und befindet sich auf Klassenebene. Man kann sie nicht ersetzen, auch wenn man ein Objekt erstellen. Mockery hat eine Lösung gefunden, um den Klassenlademechanismus zu stören, aber das ist alles andere als schön und sauber. Statische Methoden sollten heutzutage kritisch betrachtet werden. Es gibt nicht viele Anwendungsfällen, in denen statische Methoden tatsächlich Sinn ergeben.
Ähnlich wie bei final. Final ist final, Punkt. Du solltest gute Gründe haben, etwas endgültig zu machen, aber wenn du es tust, dann könntest du Probleme mit deinen Mocks haben.
Alles in allem ist schon viel gewonnen, wenn du ein modernes Programmierparadigma verwendest und dir über die Objekte (anstatt über static und final) Gedanken machst.
Ich habe es schon erwähnt: Nur was du testest, ist wirklich getestet. Zum Glück gibt es dafür eine Lösung. Aber bevor wir uns damit beschäftigen, sollten wir uns damit befassen, unsere Komponententests automatisch durchzuführen.
Kontinuierliche Integration (Continuous Integration) bedeutet, dass man sofort testet, sobald man mit dem Codieren fertig ist. In der Praxis würde das bedeuten: Wenn du etwas an GitHub (oder wen auch immer) sendest, wird es getestet, ohne dass du das Testen selbst auslöst. Idealerweise hast du deine Tests vor Ort ausgeführt, bevor du einen Push durchführst. Die Idee dahinter ist, niemandem zu vertrauen und die Tests automatisch durchzuführen.
CI führte zu vielen neuen Möglichkeiten. Sobald die Tests erfolgreich bestanden wurden, stellen einige Benutzer sie auch automatisch bereit. Warum auch nicht? Die Funktion ist fertig, wir hoffen, dass sie von unserem Testcode vollständig abgedeckt wird … lasst uns rocken. Dies wird als “Continuous Deployment” bezeichnet und spart viel Zeit. Einige Unternehmen schaffen es, hundert Mal am Tag neue Releases zu veröffentlichen.
Dazu benötigen man zunächst ein CI-Tool. Eine beliebtes CI-Tool der alte Schule ist Jenkins. Ich würde nicht empfehlen, Jenkins direkt zu verwenden, es sei denn, man verfügen über die erforderliche Personalstärke und muss alles auf einem eigenen Servern halten. Ein Ein-Mann-Unternehmen wie ich oder kleinere Unternehmen sollte sich die verwalteten Hostings ansehen.
Ein CI-Server, den ich mag, ist CircleCI. Eine andere beliebte Alternative ist Travis. Ich habe sie nicht im Detail verglichen, aber ich glaube, dass sie sich sehr ähnelich sind. Ich habe mich für Circle entschieden, weil sie für den Anfang einen tollen Preis anbieten und super nette Leute sind. Es ist deine Entscheidung.
So sieht CircleCI aus, wenn ich etwas in mein Repository schreibe:
Du musst den Code lediglich an GitHub oder Bitbucket senden, ein Konto bei Circle eröffnen und ein jeweiliges Projekt aus der Liste auswählen, die Circle bereitstellt. Drücke einfach “build” und damit ist es dann normalerweise schon erledigt.
In einigen Fällen benötigst du möglicherweise eine Erweiterung. In diesem Fall kannst du Circle mithilfe einer Datei mit dem Namen circle.yml sehr detailliert konfigurieren. Lege diese Datei einfach in deinen Stammquellordner, dann kannst du eine Menge Dinge überschreiben.
So sieht meine Circle.yml aus:
machine:
php:
version: 5.6.17
dependencies:
pre:
- sudo composer selfupdate
cache_directories:
- "vendor"
test:
override:
- vendor/bin/phpunit --bootstrap ./tests/bootstrap.php tests
Der Teil “machine” konfiguriert meine PHP-Version. Es gibt eine Liste der von Circle bereitgestellten Versionen. Normalerweise sind es die Versionen, die im Standardumfang von Ubuntu enthalten sind. Wenn nicht, dann kannst du Circle sogar so konfigurieren, dass es seine eigenen Dinge erstellt, aber das wäre ziemlich … schwierig.
Es ist jedoch einfach, einige Shell-Befehle als “dependency” hinzuzufügen. In meinem Fall wollte ich, dass composer für meine Tests auf dem neuesten Stand ist. Ich habe dazu den Selbstaktualisierungsbefehl in den “pre”-Bereich hinzugefügt.
Auch die cache_directories sind eine Einstellung, die ich nicht missen möchte. Man kann die Tests erheblich beschleunigen, indem man die Abhängigkeiten von composer zwischenspeichert. Bei Bedarf können auch weitere Ordner zwischenspeichert werden.
Schließlich wollte ich meinen Testbefehl festlegen. Aus diesem Grund habe ich mit dem Befehl “override” etwas in den Abschnitt “test” eingefügt.
Die Dokumentation ist ziemlich umfangreich und mit Circle kann man viele Dinge machen. Aufgrund der günstigen Preise für kleine Teams ist es für viele eine gute Alternative.
Eine erstaunliche Eigenschaft ist, dass man damit auch Datenbanken einrichten und testen kann. Lies weiter.
Normalerweise nenne ich es “Integrationstest”, aber ich habe erfahren, dass das nicht präzise ist. Was ich damit meine ist, die vollständige Integration von Code und Datenbank zu testen: also alle beteiligten Komponenten. Als ich anfing, als Programmierer zu arbeiten, war das Testen der Integration ein harter Job. Normalerweise hatten wir nur eine Hand voll Testfälle, um zu sehen, ob alles gut zusammenpasste, bevor wir es in Produktion nahmen.
Heutzutage ist diese Art des Testens dank Tools wie CircleCI viel einfacher. Es ist fast schmerzfrei, eine Datenbank auf einer Test-VM einzurichten und einige Tests durchzuführen. Das Testen mit diesem Ansatz nimmt natürlich einige Zeit in Anspruch. Aber wenn du mich fragst, Mock-Tests sind nur ein Teil der Aufgabe, es muss auch einen echten Datenbanktest geben.
Wenn du eine Datenbank benötigst, musst du zuerst eine installieren. Für diesen Blog verwende ich MySQL und du kannst aber auch eine anderes Datenbankmanagementsystem deiner Wahl verwenden. Ich habe eine Datenbank namens “phpunittest” erstellt.
Dann erstelle ich einen Benutzer wie diesen:
CREATE USER 'ubuntu'@'%' IDENTIFIED BY '';
Ich weiß bereits, dass dies der Standardbenutzer von CircleCI zum Testen ist. Also benutze ich ihn auch lokal, um die Dinge einfach zu halten. Dann gab ich dem neuen Benutzer einige Rechte meiner Datenbank, so:
GRANT Create Routine, Insert, Select, Drop, Delete, Index, Create, Update, Alter ON `phpunittest`.* TO `ubuntu`@`%`;
FLUSH PRIVILEGES;
Soweit zu den MySQL-Interna. Ich mag diese Admin-Sachen nicht, aber zum Glück sollten diese Datenbanken keine vertraulichen Daten enthalten und nur vorübergehend sein. Also sind die Berechtigungen so in Ordnung.
Als nächstes müssen wir die Datenbank befüllen. Ich habe drei Skripts erstellt: eines zum Erstellen der Tabellen, eines zum Auffüllen einiger Testdaten und eines zum Löschen des gesamten von mir erstellten Mülls.
Erstelle folgendes in tests/assets/create.sql:
DROP TABLE IF EXISTS `persons`;
CREATE TABLE `persons` (
`id` int NOT NULL,
`email` varchar(100),
`nick` varchar(100),
PRIMARY KEY (`id`)
);
Es ist eine einfache Tabelle, die Personen speichern soll. Vielleicht möchtest du sie optimieren, du könntest zum Beispiel auto_increment verwenden.
Als nächstes kommen einige Daten hinzu (tests/assets/data.sql):
insert into `persons` ( `email`, `id`, `nick`) values ( 'test1@example.com', '1', 'Test 1');
insert into `persons` ( `email`, `id`, `nick`) values ( 'test2@example.com', '2', 'Test 2');
Dies sind meine grundlegenden Testdaten. Du solltest in Betracht ziehen, weitere Daten innerhalb der Testfällen hinzuzufügen, da die Verwaltung der Daten in einer Datei wie data.sql verwirrend und schwierig sein kann. Ich gebe hier in der Regel nur die unbedingt notwendigen Daten ein, ohne die die Anwendung überhaupt nicht laufen würde.
Zum Schluss noch ein Bereinigungsskript:
set foreign_key_checks=0;
TRUNCATE `persons`;
set foreign_key_checks=1;
Fremdschlüssel sind mir beim Aufräumen nicht wichtig. Also habe ich sie deaktiviert, alles gelöscht und dann wieder aktiviert.
Now I got a couple of commands which need to be executed before test runs. To make sure it is happening, I created myself a shell script. It’s optional, but I recommend to do something similar, so your steps are repeatable and your colleagues keep their mental sanity. Script everything!
Jetzt habe ich ein paar Befehle, die vor den Testläufen ausgeführt werden müssen. Um sicherzugehen, dass das auch passiert, habe ich mir ein Shell-Skript erstellt. Es ist optional, aber ich empfehle, es ähnlich zu machen, damit die Schritte wiederholbar sind und deine Kollegen ihre geistige Gesundheit bewahren. Verwende Scripts!
Hier ist run-tests.sh:
#!/usr/bin/env bash
mysql -u ubuntu -D phpunittest < tests/assets/teardown.sql
mysql -u ubuntu -D phpunittest < tests/assets/create.sql
mysql -u ubuntu -D phpunittest < tests/assets/data.sql
vendor/bin/phpunit tests
Mein SQL-Code wird ausgeführt, bevor die Tests starten.
Anschließend können wir endlich Code zum Testen der Datenbank hinzufügen. Wie bereits erwähnt, bin ich ein großer Fan von Phormium. Ich habe es also zu meinen Abhängigkeiten hinzugefügt:
"require": {
"phormium/phormium": "~0.8.0"
},
Dann einfach auf “composer install” klicken. Phormium strömt herein :-)
Jetzt kommt hier ein Anwendungscode. Hier ist zunächst mein Modell, das von Phormium abgeleitet wird.
class Person extends \Phormium\Model
{
protected static $_meta = array(
'database' => 'phpunittest',
'table' => 'persons',
'pk' => 'id'
);
public $id;
public $nick;
public $email;
}
Siehe GitHub.
Das Meta-Zeug ist nur für Phormium. Es weiß dann, welche Datenbank und Tabelle ich meine und ich achte auch darauf, dass der Primärschlüssel bekannt ist, um den Datensatz für Aktualisierungen und Löschvorgänge zu identifizieren. Dann gibt es nur einige öffentliche Felder, wie sie in der Datenbank benannt sind. Es ist Phormium-Magie – aber natürlich kannst du hier auch dein eigenes Ding verwenden.
class App
{
private $config = ["mysql:host=127.0.0.1:3306;dbname=phpunittest", "ubuntu", ""];
public function __construct(array $config = null)
{
if (!empty($dsn)) {
$this->config = $config;
}
list($dsn, $user, $pass) = $this->config;
\Phormium\DB::configure([
'databases' => [
'phpunittest' => [
'dsn' => $dsn,
'username' => $user,
'password' => $pass
]
]
]);
}
public function readPersons()
{
return Person::objects()->fetch();
}
}
Siehe GitHub
Eine Menge Code, aber keine Sorge. Wenn du Phormium nicht verwenden möchten, musst du es nicht vollständig verstehen. Was ich tue, ist, die Datenbankverbindung innerhalb des Konstruktors aufzubauen. Ich habe einige Standardeinstellungen für die Datenbankkonnektivität. Diese bestehen aus einer Verbindung zu 127.0.0.1 (localhost), meinem Datenbanknamen, dem Benutzer und dem Passwort. Die Methode “readPersons” fordert Phormium auf, alles aus der Personentabelle zurückzugeben.
So weit, ist es gut. Nun, wie kann man es testen? Es ist eigentlich nicht kompliziert. Hier ist der Testfall:
class AppTest extends \PHPUnit_Framework_TestCase
{
public function testReadPersons()
{
$app = new App();
$this->assertCount(2, $app->readPersons());
}
}
Siehe GitHub
Stell sicher, dass du es mit dem Laden der Daten ausführst!
./run-tests.sh
PHPUnit 5.7.3 by Sebastian Bergmann and contributors.
....... 7 / 7 (100%)
Time: 80 ms, Memory: 6.25MB
OK (7 tests, 16 assertions)
Jetzt haben wir es auf unserer lokalen Maschine zum Laufen gebracht. Ich denke, es ist an der Zeit, sicherzustellen, dass es auch auf der CircleCI-Box läuft.
CircleCI unterstützt verschiedene sofort einsatzbereite Datenbanken. Unter ihnen sind PostgreSQL, MongoDB, Couchbase und so weiter. Natürlich steht auch MySQL zur Verfügung.
Standardmäßig erstellt CircleCI ein Schema mit dem Namen “circle_test”. Es ist nett von Circle, das zu tun, aber wenn man viele Projekte hat, dann möchte man vielleicht auch verschiedene Namen für die Schemata haben. Andernfalls kann es frustrierend sein, die Tests auf dem lokalen Computer auszuführen.
Um sicherzustellen, dass das erforderliche Schema mit dem Namen “phpunittest” verfügbar ist, benötigen wir ein weiteres SQL-Skript. Ich habe es in tests/assets/setup-db.sql abgelegt.
CREATE DATABASE IF NOT EXISTS phpunittest;
Das ist keine Hexerei. Das Skript prüft nur, ob es ein Schema mit dem Namen phpunittest gibt und wenn nicht, dann erstellt es eines. Wir brauchen das nicht für unsere lokalen Tests, da ich keine Testschemata von meinem Computer lösche. Wenn du das anders handhabst, dann musst du deine run-tests.sh anpassen.
Damit dies funktioniert, musst du wissen, dass die CircleVM einen Standardbenutzer für Datenbanken bereitstellt. Sein Benutzername ist “ubuntu” und das Passwort ist leer. Aus diesem Grund haben wir einen solchen Benutzer für unsere lokale Testmaschine erstellt. Du könntest zusätzliche Benutzer in deinen Testskripts erstellen, aber für mich ist das zu mühsam.
Um diese Skripts auszuführen, müssen wir erneut die Datei circle.yml anpassen.
Erinnerst du dich, als wir einen Befehl hinzugefügt haben, um composer zu aktualisieren? Wir befinden uns hier im gleichen Abschnitt:
dependencies:
pre:
- sudo composer selfupdate
- mysql -u ubuntu -D circle_test < tests/assets/setup-db.sql
- mysql -u ubuntu -D phpunittest < tests/assets/create.sql
- mysql -u ubuntu -D phpunittest < tests/assets/data.sql
Ich benutze MySQL direkt, um meine Skripts einzuleiten. Eine Sache, die dich verwirren könnte ist, dass das Skript setup-db.sql in das Standardschema weitergeleitet werden muss. Der Grund dafür ist, dass ich zunächst eine Datenbank auswählen muss, sonst funktioniert der Befehl nicht. Daher habe ich mich für circle_test entschieden, weil ich mir sicher bin, dass es immer da ist. Selbst wenn ich etwas vermassle, wird nichts Schlimmes passieren. Ich bin mir nicht so sicher, ob ich ein MySQL-Informationsschema oder ähnliches verwende.
Das war’s, meine Tests wurden erfolgreich abgeschlossen. Um ehrlich zu sein, ist es immer ein bisschen anstrengend, so etwas zu machen, aber die Mühe lohnt sich auf jeden Fall. Die Dokumente von CircleCI sind größtenteils exzellent und ihre Unterstützung und Community sind ganz hilfreich.
Es gibt noch eine letzte Sache, die ich für unsere kleine Testrunde erwähnen muss. Es ist Code Coverage. Anfangs sagte ich, dass 100% erstaunlich wären, aber in der realen Welt hat man so eine Abdeckung so gut wie nie. Aber woher kannst du wissen, wie viel du bereits abgedeckt hast?
Google nach “Code Coverage” und du hast die Antwort. Es gibt viele Werkzeuge dafür. Sogar deine IDE unterstützt es möglicherweise. Die Art und Weise, wie die Abdeckung quantifiziert wird, ist von Werkzeug zu Werkzeug unterschiedlich. Du solltest dir also mehrere ansehen und dich dann auf eines festlegen.
Für PHPUnit muss xdebug auf deine System aktiviert sein. So kannst du herausfinden, ob du es bereits hast:
$> php -ini|grep 'xdebug support'
xdebug support => enabled
Wenn es nicht vorhanden ist, musst du es zuerst installieren.
Wenn das erledigt ist, ist der Rest einfach. Verwende einfach einen Befehl wie diesen:
vendor/bin/phpunit tests --coverage-html target --whitelist=tests
Es gibt einen zusätzlichen Parameter namens –coverage-html. Es nimmt den Zielordner als Parameter entgegen. Außerdem benötigst du den Parameter –whitelist, der PHPUnit mitteilt, was in die Berichterstattung aufgenommen werden soll.
Im Zielordner siehst du eine generierte statische Website mit den erforderlichen Nummern.
Wie du oben siehst, haben wir den größten Teil unseres Codes bereits behandelt! Das Datenbankpaket könnte besser sein. Nach ein paar weiteren Klicks sehe ich, wo das Problem liegt.
Ich habe noch nie einen bestimmten Teil meines Codes getestet! Bei den Komponententests wurde dieser Code nicht ausgeführt, daher ist er rot markiert. Es ist Zeit, den Code zu überdenken oder weitere Tests zu schreiben.
Ich empfehle, dieses Diagramm regelmäßig, mindestens jedoch wöchentlich, anzusehen.
Es gibt sogar Tools, die automatisch melden, wenn sich die Codeabdeckung im Laufe der Zeit verschlechtert. Es gibt unzählige Möglichkeiten!
Dieser Blog-Beitrag hat zum Ziel, dir PHPUnit und Unit-Tests im Allgemeinen vorzustellen. Ich hoffe, ich habe das Ziel erreicht und du bist jetzt in der Lage selbstständig weiter zu machen, indem du die spezifische Dokumentation ließt. Wir sprachen darüber, wie man mit PHPUnit testen kann, über die automatisierten Test mit CircleCI und über das Testen von Datenbanken.
Es gibt noch viel mehr, das aber nicht in einen einzelnen Blog-Beitrag passt. Man könnte ein ganzes Buch darüber schreiben, aber im Ernst, es besteht keine Notwendigkeit dafür. Unit-Tests sind nicht wirklich kompliziert, wenn du erst einmal den richtigen Weg eingeschlagen hast. In den meisten Fällen hilft dir die offizielle Dokumentation auf deinem Weg.
Wenn du noch Fragen oder Vorschläge zur Verbesserung dieses Artikels hast, lass es mich bitte wissen.
Und wenn du durch das Lesen etwas Nützliches gelernt haben, wäre ich dir dankbar, wenn du es mit deinen Freunden über Twitter, Facebook oder etwas Ähnlichem teilen würdest.
Tags: #PHP #PHPUnit #Unit Testing #CircleCI #Code Coverage #Database testing #Mockery #Mock Objects