You are currently viewing TDD Tutorial PHPUnit – Testgetriebene Entwicklung

Dies ist ein Gastartikel von Roland Golla.

TDD – Test Driven Development Example

Automatisierte Tests mit TDD – Test Driven Development sind der Schlüssel zu einer effizienten und nachhaltigen Art des Programmierens. Da der PHP-Programmierer-Stundensatz auch in den letzten Jahren stetig angestiegen ist, ist es wichtig, die Stundenzahl bei der Softwareentwicklung gering zu halten. Das bedeutet vor allem, die Effizienz zu steigern.
Roland GollaDie Kosten und der Ärger mit einem Bug steigen exponentiell mit der Zeit bis zu seiner Entdeckung. Wenn er direkt bei der Entwicklung gefunden wird, kostet es praktisch nichts ihn zu beheben. Der Entwickler ist gerade so oder so mit seinem Werkzeug und seinem Kopf an der entsprechenden Stelle unterwegs. Wird er aber erst auf Staging-Systemen von Testteams gefunden, dann entsteht schon mal einiges an Kommunikationsaufwand. Und natürlich ein erhöhter Personalaufwand.
Dazu kommt das Problem, dass der Programmierer zu dem Zeitpunkt schon längst an einer anderen Stelle arbeitet. Das ist trotz Git auch immer ein logistisches Problem und es muss sich auch immer erst wieder an der Stelle eingearbeitet werden, was auch physisch eine hohe Belastung ist.
Im Live-System kann darüber hinaus natürlich auch ein wirtschaftlicher großer Schaden entstehen, der sich dann auch negativ auf die Kundenbeziehung auswirkt. Vor allem im Bereich E-Commerce kann das mit einer entsprechenden Kampagne im Weihnachtsgeschäft auch ganze Existenzen bedrohen.

Bugs braucht keiner mehr – es werden keine Stunden mehr verkauft

Lange Jahre haben vor allem große Internetagenturen schlechten und fehleranfälligen Code produziert, damit später möglichst viele Support-Stunden abgerechnet werden können. So hat man sich das Geld aus der Angebotsphase wieder zurück geholt.
Aus dieser Zeit stammen sehr viele der heutigen Entwickler. Die werden zwar auch sehr stark gesucht, allerdings hat die Web-Development-Sparte aufgrund ihrer schlechten Qualitätsansprüche ein echtes Nachwuchsproblem. Allerdings ist die Branche hier gerade selbst stark im Umbruch.
Automatisierte Prozesse finden im Frontend und Backend immer mehr Beachtung und sind grundsätzlich schon lange ein elementarer Bestandteil. In diesen Strukturen ist es ein Leichtes, Tests hinzuzufügen. Gerade Unit-Tests sind dabei sehr praktisch, da sie keine lauffähige Umgebung für ihre Ausführung brauchen.
Junge Kunden aus der Startup-Szene, die ihr Geschäftsmodell nahezu ausschließlich im Internet abbilden, wollen eine zuverlässige und günstige Software. Bugs, gerade im Bereich E-Commerce, können extrem schnell verheerenden Schaden anrichten. Ein fehlerhafter Preisimport, kaputte Produktkonfiguratoren oder eine fehlerhafte Validierung von Pflichtfeldern kann man Kunden nicht in Rechnung stellen. Werden diese wichtigen Geschäftsprozesse nicht mit Tests abgebildet, ist das schlicht fahrlässig.

PHPUnit Test Tutorial mit dem Codeception Testing Framework

PHP-Frameworks sollen Entwicklern die Möglichkeit geben, komplexe Dinge einfach über einen Library-Befehl auszuführen. Heute geht es um PHPUnit Tests und die haben natürlich schon eine hervorragende und stark sprechende Methoden-Schreibweise. Hier ist die Frage, warum man das noch in Codeception wrappen sollte, völlig berechtigt.
Der USP ist die einfache Art Mock-Objekte zu schreiben. Das unten aufgeführte Codeception Unit-Test Example veranschaulicht das sehr gut. Des weiteren wird PHPUnit als Module geladen und vererbt nutzt. Für die beliebten Assertions braucht man also keine neue Syntax zu lernen und kann auch auf die sehr gute Dokumentation von PHPUnit selbst zurückgreifen.
Das Codeception Framework bietet große Vorteile beim Einsatz von unterschiedlichen Testarten und ist besonders gut in API- und Acceptance-Testing. Dazu kann es auch einfach über alle Testarten ein gemeinsames Reporting generieren, was sonst so nicht möglich wäre und für eine Dokumentation unerlässlich ist.

Features werden verkauft und programmiert – Bugs braucht keiner

Softwarequalität wird nicht bezahlt, bietet aber einen großen Wettbewerbsvorteil. Ganz davon abgesehen, dass es auch viel bessere Arbeitsbedingungen schafft, die immer gefragter werden. Und es gibt auch in Legacy-Projekten genug Gelegenheit Tests einzusetzen.
PHPUnit ist hervorragend Abwärtskompatibel. Codeception ist nicht abwärtskompatibel und braucht mindestens PHP5.6. Es ist allerdings auch noch eine recht junge Open-Source-Software. Hier wurde in den letzten Updates viel Wert auf Stabilität gelegt.

Bugs kosten mehr als nur Geld und bringen auch keins

Bugs machen Ärger. Obwohl sie im CMS-Bereich eher weniger durch das fehlende Interesse der Kunden auffallen, sind sie im Bereich E-Commerce schnell schmerzhaft und ziehen ganz ernste Meetings und Telkos in den schönen Arbeitsalltag.
Ob man Tests immer von Anfang an einbauen muss und wie viel man testet ist eine Frage, die nicht von Nicht-Test-Erstellern beantwortet werden sollte. Fakt ist, dass sie viel Zeit kosten und auch immer in eine echte Legacy-Code-Hölle führen. Refactoring natürlich ausgeschlossen, denn ohne Tests kann man hier keine Änderungen durchführen. Nobody moves nobody gets hurt.
Das macht Code-Chaos und erhöht den Druck auf die Entwickler, unter diesen Umständen weiterhin zu liefern. Der wirtschaftliche Spätschaden durch extrem hohe Wartungs – und aktuellen Entwicklungskosten steht dann nicht mehr in der Relation zum Anschaffungswert, als die Welt noch in Ordnung und alle Beteiligten gut drauf waren. Und natürlich kippt dann auch die schon angespannte Stimmung noch mehr, wenn Entwicklern die Arbeit keinen Spaß mehr macht. Ein Teufelskreis.

PHPUnit ein erster Test – Formular Validator als Feature

Generell machen PHPUnit Tests immer da Sinn, wo Logik geschrieben wird. Bestimmte Parameter werden übergeben, Daten geholt und verarbeitet und etwas zurück gegeben. Und natürlich braucht man keine Getter und Setter in Entities zu testen um künstlich eine Testabdeckung hochzufahren. Die kann man bei der Code Coverage übrigens auch ausschließen.
Bei dem Formular ist die erste Anforderung eine Spam Protection. Diese soll nach Auffälligkeiten wie ‘viagra’ oder anderen Spam-Keywords aus einer CSV-Liste
ausschau halten.
Test-Driven bedeutet, das vorher schon als Test abzubilden. Im Grunde kann man durch gute Tests für einen anderen Entwickler genau definieren was zu tun ist. Es müsste also nicht dieselbe Person sein.
Man kann Tests also doch zentral schreiben lassen, wenn man diese nicht nachträglich implementieren muß. Denn das wird sehr schwer und am Ende auch keiner mehr machen.

class spamProtectionCest
{
    private $fixture;
    public function _before(UnitTester $I)
    {
        $this->fixture = new SpamProtection();
    }
    // tests
    public function validateUserInputsReturnTrueOnValidData(UnitTester $I)
    {
        $this->fixture = Stub::make($this->fixture, ['validateName' => true, 'validateEmail' => true, 'validateMessage' => true, 'validateIp' => true]);
        $data = ['name' => '', 'email' => '', 'message' => '', 'ip' => ''];
        $I->assertTrue($this->fixture->validateUserInputs($data));
    }
}

Hier ist der erste Test, der auch dem entspricht, was ein Entwickler im Backend als erstes zur Lauffähigkeit schreiben würde. Eine Public Methode, die User Eingaben validiert und wenn alles passt “true” zurück gibt und sonst “false”. Stub-Objekte müssen zu diesem Zeitpunkt natürlich nicht in der Applikation vorhanden sein.
Hier wird lediglich das Verhalten von Abhängigkeiten simuliert und zwar genau für die oben genannte Feature-Beschreibung. Jedes Unit bekommt seinen eigenen Test. Mehr noch: für jede Feature-Anforderung gibt es einen separaten Test. So werden Methoden in der Regel durch 3 Tests abgedeckt.
Mehr sollte ein Unit auch nicht tun. Das hier ist der erste Test. Der Grün-Test. Wenn alles in Ordnung ist, dann kommt ein “Ok” zurück. Ein Test für Validate Name bekommt natürlich Tests für die Eingabe von leeren Strings und dem Abgleich mit einer Spam Keyword Blacklist. Das letzte wäre allerdings bereits ein Functional Test eng angelehnt an den Unit-Test.

public function validateNameEmptyIsFalse(UnitTester $I)
{
    $methodReturn = $this->getMethodReturn('validateName', '');
    $I->assertFalse($methodReturn);
}
public function validateNameIsSpam(UnitTester $I)
{
    $methodReturn = $this->getMethodReturn('validateName', 'viagra');
    $I->assertFalse($methodReturn);
}

Natürlich gibt es zu der validateUserInputs 4 weitere Tests, die je eine validate-Methode fehlschlagen lassen. Damit sind alle Features von der Methode abgedeckt.
Auffällig ist hier, dass auf protected Methoden zugegriffen wird. Denn es gibt nur eine Public Methode. Da kann ich aber nur einzeln über Mocks festlegen, dass, wenn die Validate Name “false” zurück gibt, auch ein “false” zurückgegeben, wird. Ob das eine Feature für ‘viagra’ im Namen funktioniert, kann ich so nicht testen. Und deshalb teste ich diese Methoden genauso, denn ich möchte genau wissen wo der Bug steckt und mich nicht weiter durch Tests suchen müssen.

/**
 * @throws \ReflectionException
 */
private function getMethodReturn($method, $param)
{
    $class = new ReflectionClass($this->fixture);
    $method = $class->getMethod($method);
    $method->setAccessible(true);
    $methodReturn = $method->invoke($this->fixture, $param);
    return $methodReturn;
}

Jenkins Devops – Geschwindigkeit ist wichtig

Tests, die Zeit brauchen, werden nicht ausgeführt. 305ms. 12 Tests mit 16 Assertions. Kann man direkt in PHPStorm mit Code Coverage ausführen. Dabei sollte einen Code Coverage nicht so wichtig sein, denn Zeilen über die Ausführung von Tests zu erreichen und dadurch grün zu bekommen bedeutet nicht, dass die Tests aussagekräftig sind und Fehler finden.
Wenn man allerdings später bestehenden und getesteten Code mit einem Code Refactoring optimiert und verbessert, kann man sich die Ausführung der Tests mit der Speicherfunktion in der IDE verbinden. Dann weiß man direkt, ob noch alles geht ohne das man die Tests manuell anstoßen muß. Denn dann macht man das auch nicht mehr irgendwann am Tagund muß eventuell mehrere Schritte zurückgehen. Aber damit das geht müssen Tests schnell sein.

Tests dokumentieren Code

Viele Zeilen Code werden auch deshalb nicht mehr angefasst, weil keiner wirklich weiß, was genau dieser eigentlich tut. Mit der Zeit werden die neuen Anforderungen hier zeilenweise zwischengeschoben. Dadurch tun Methoden am Ende viel mehr, als sie eigentlich sollen.
Solche großen Methoden haben viele Verzweigungen im Code und deshalb eine sehr hohe „Komplexibilität“. Entwickler können das ganze nur sehr schwer erfassen und müssen eine hohe Anstrengung auf sich nehmen, um sich in einen solchen Code reinzudenken. Hier herrscht hoher Druck, denn Fehler lassen sich hier kaum vermeiden.
Auch das der Code bereits Fehler hat ist, sehr wahrscheinlich. Wenn man allerdings Tests hat, so reicht es in der Regel, diese zu lesen und damit zu verstehen welche Ziele mit dem Code verfolgt werden. Dadurch ist es sogar möglich. den Code komplett zu ersetzen und gleichzeitig zu gewährleisten, dass alles weiter funktioniert. Dieser Komfort ist ein echter Wettbewerbsvorteil.

Fazit Test Driven Development

Glauben ist nicht Wissen. Tests sparen Zeit und Geld. Sie verbessern die Beziehung zum Kunden und bringen Empfehlungen und Folgeaufträge. Sie schaffen gute Arbeitsbedingungen für Entwickler und machen einen Arbeitgeber im derzeitigen Markt attraktiver.
Hier schläft die Konkurrenz allerdings nicht und dieser USP ist klar erkannt. Und doch gibt es immer wieder ein Zögern und auch immer wieder einen Rückfall in alte Gewohnheiten. Das ist rational nicht zu erklären.
TDD bietet eine Chance, schnell und einfach in Tests einzusteigen und vor allem gezielt Wissen und Erfahrung aufzubauen. Denn bestehende Legacy-Applikationen mit Tests abzubilden ist kein Weg für Einsteiger. Jede Anforderung an eine Software kann als Test formuliert und dann umgesetzt werden.
Dadurch implementiert man auch nur noch die konkrete Anforderung und nicht mehr einen Wust von möglichen Spekulationen und Entwickler-Interpretationen. Und am Ende ergibt sich aus den Tests natürlich auch eine Code-Dokumentation über die konkreten Feature-Anforderungen. Und wer Tests einmal als Werkzeug verstanden hat, der wird sie auch immer einsetzen.