So entwicklen wir testgetrieben mit Symfony in München
Bei uns gibt es keine glänzenden Management-Präsentationen oder Marketing-Versprechen! Auf dieser Seite möchten wir mit einfachen Code-Beispielen aufzeigen, wie wir testgetrieben Webseiten entwickeln.
Diese Frage lässt sich am Besten anhand eines kleinen Code-Beispiels beantworten:
Data-Klasse
Die Klasse Data wurde nach dem weitverbreiteten „einfach-mal-Losprogrammieren“-Konzept implementiert.
Würden wir im nächsten Schritt ein data-Objekt erstellen und dem Konstruktor beim Initialisieren ein
Array von Objekten übergeben, so könnten wir über die get()-Funktion auf diese zugreifen. Was aber würde
passieren, wenn:
wir dem Konstruktor kein Array von Objekten übergeben,
die Objekte nicht public $blist enthalten oder
die Klasse erweitert werden muss?
Tritt einer der ersten beiden Fälle ein, dann bricht der Programmdurchlauf mit einer relativ
unspezifischen Fehlermeldung ab. Bei einer geringen Menge Code reicht diese Fehlermeldung vielleicht
noch aus, um die
betreffende Stelle im Code ausfindig zu machen und das Problem zu beheben. Je komplexer der Code im
Laufe des Entwicklungsprozesses aber wird, desto schwieriger bzw. zeitaufwändiger (und damit kostenintensiver) wird
eine solche Fehlerbehebung. Es sollte also immer oberstes Gebot sein, die Code-Komplexität so gering wie möglich zu
halten und mögliche Fehler aussagekräftig abzufangen. Wollen wir die Klasse erweitern, so sollte
außerdem sichergestellt
werden, dass bestehende Funktionalitäten erhalten bleiben.
Was ist testgetriebene Entwicklung genau?
Um aussagekräftige Fehlermeldungen und eine störungsfreie Weiterentwicklung sicherzustellen, setzen wir auf testgetriebene Entwicklung: Bevor wir
auch nur eine Zeile Code schreiben, definieren
wir über Tests bereits im Vorfeld unsere Erwartungshaltung, damit wir später genau das Richtige implementieren.
Diese Methode folgt dabei stets dem RED » GREEN » REFACTOR-Zyklus.
RED
Da wir den Test vor der eigentlichen Implementierung
schreiben, schlägt dieser zuerst immer fehl.
GREEN
Durch die Implementierung erfüllen wir dann unsere
Testbedingungen und bringen den Test so zum Laufen.
REFACTOR
Im letzten Schritt erfolgt das Aufräumen. Hier wird der
Code hinsichtlich der Lesbarkeit und Performance optimiert.
Dieser Zyklus wird ständig wiederholt. Im Folgenden wenden wir das Ganze für unsere data-Klasse unter
Verwendung von PHPUnit an. PHPUnit bietet eine Sammlung von Methoden, welche
zum Schreiben von UnitTests benötigt werden. Ein UnitTest soll immer nur einen sehr kleinen Teil (ein Unit)
des Codes testen. UnitTests werden verwendet, um das Verhalten einer Komponente
isoliert, d. h. ohne ihre Abhängigkeiten zu anderen Komponenten, zu überprüfen.
Nun entwickeln wir die oben genannte Data-Klasse testgetrieben:
Erste Iteration
RED
Im ersten Zyklus-Schritt wird der Test für die Data-Klasse geschrieben, welche wir im Folgenden als
ProductList-Klasse spezifizieren:
ProductList-Test
Die Testklasse ProductListTest erbt die für den UnitTest benötigten Methoden von
\PHPUnit_Framework_TestCase. Wird jetzt der Test ausgeführt, so schlägt dieser fehl, da die Klasse ProductList noch
nicht existiert:
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.
GREEN
Der Test gibt uns also den nächsten Arbeitsschritt vor: Wir müssen die Klasse ProductList
implementieren:
ProductList-Klasse
Jetzt führen wir den Test erneut aus:
OK (1 test, 0 assertions)
Der Test funktioniert! Als nächstes erweitern wir unseren Test um die gewünschten Funktionalitäten von
ProductList.
Die Klasse ProductList soll eine Funktion getListedProducts() enthalten und jedes Produkt muss außerdem
eine Funktion isListed() besitzen –
das schließen wir quasi vertraglich über ein Interface ProductListInterface ab:
ProductList-Interface
Als Nächstes wird der Test erweitert. Im jetzten Zustand gibt es keine Verbesserungen am Code, so dass wir den Schritt Refactor überspringen können.
Zweite Iteration
RED
Mit Hilfe der prophesize-Funktion prophezeien wir ein Objekt der Klasse ProductListInterface.
Das resultierende listedProduct ist ein Objekt der Klasse ObjectProphecy, welches das zukünftige
Verhalten einer Instanz von ProductListInterface
beschreiben soll. Durch den Aufruf von isListed()->willReturn(true)/(false) erzeugen wir uns das
gewünschte Verhalten eines gelisteten und eines nicht gelisteten Produkts.
Die beiden Prophezeiungen werden dann in der ProductList gespeichert und über ->reveal() aktiviert bzw.
in ein dummy-Objekt überführt. Dieses versucht dann die Prophezeiung zu erfüllen.
Zum Schluss werden über Assertions die Erwartungen mit dem Ist-Zustand verglichen:
Wird wirklich nur ein gelistetes Produkt zurückgegeben?
Enthält productList auch das übergebene listedProduct?
Jetzt wird der ProductListTest erneut ausgeführt. Dieser schlägt wieder fehl, da die Klasse ProductList
noch keine Funktionen enthält.
There was 1 error:
1) Tests\Demo\ProductListTest::testProductList
Error: Call to undefined method Demo\ProductList::getListedProducts()
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.
GREEN
Jetzt müssen wir die Klasse ProductList entwickeln, sodass unsere im Test formulierten Erwartungen erfüllt werden:
Testgetriebene ProductList-Klasse
Zum Vergleich: Data-Klasse
Gerade in der Gegenüberstellung wird die deutliche Verbesserung der Code-Qualität mittels der testgetriebenen Entwicklung sichtbar.
Fazit
Die durch testgetriebene Entwicklung implementierte ProductList-Klasse erfüllt jetzt alle Testbedingungen. Sie
ist leicht erweiterbar, da die Tests die bereits bestehende Logik absichern. Da die Produkte das
ProductListInterface
implementieren müssen, ist außerdem sichergestellt, dass isListed() vorhanden ist. Außerdem kann nur noch
ein Array an den Konstruktor übergeben werden. Der direkte Vergleich zur anfänglichen Data-Klasse soll
verdeutlichen, warum es sich
lohnt, testgetrieben zu arbeiten und den Weg des qualitativ hochwertigen Codes zu gehen.
Mit diesem Beispiel haben wir die testgetriebene Entwicklung veranschaulicht und gehen jetzt im zweiten Teil speziell auf das Symfony Framework ein.
Symfony ist ein PHP Framework, welches in erster Linie eine Sammlung von vorgefertigten, schnell
integrierbaren Softwarekomponenten
bereitstellt. Das bedeutet kurz und knapp weniger Code schreiben und ein geringeres Fehler-Risiko. Im Umkehrschluss resultiert daraus
mehr Zeit für die wesentlichen Entwicklungspunkte der Webapplikation. Der zweite wichtige
Aspekt bei der Entwicklung mit Symfony ist eine strukturierte Methodik – quasi eine Baugerüst
für Webapplikationen. Durch die Vorgabe und Einhaltung von Best Practices kann eine stabile, wartbare
und erweiterbare Software erzielt werden.
Symfony
ist Open Source,
besteht aus insgesamt 36 einzelnen PHP Bibliotheken,
bildet basierend auf den Komponenten ein vollständiges Framework,
wird in vielen Projekten verwendet und
hat über 300.000 aktive Entwickler.
Am besten lassen sich die zahlreichen Vorteile von Symfony anhand kurzer Beispiele aufzeigen.
Nachfolgend zeigen wir am Beispiel einer Newsletter E-Mail, welche Probleme Symfony sehr einfach für uns lösen kann.
Beispiel: Versand einer Newsletter E-Mail
Wieder starten wir nach dem weitverbreiteten „einfach-mal-Losprogrammieren“-Konzept.
Der Klasse Newsletter wird ein Array von Empfängern übergeben. Innerhalb der Newsletter-Klasse wird dann
ein Mailer instanziiert, und dessen send-Funktion für
jeden Empfänger ausgeführt.
Der Programmablauf gestaltet sich wie folgt:
Klasse Mailer
Klasse Newsletter
Sehen wir uns die Newsletter Applikation genauer an, so finden wir eine direkte Abhängigkeit der
Newsletter-Klasse von Mailer, da dieser
Klassenintern instanziiert wird. Das macht die Softwarekomponente nicht testbar, erweiterbar und
wiederverwendbar. Diese nicht testbaren
Abhängigkeiten machen ein Fehlerhandling so gut wie unmöglich. Zum Glück können wir dieses Problem
beheben, indem wir den Symfony-Weg testgetrieben beschreiten.
Optimierungspotentiale beider Klassen
Dependency Injection löst die Abhängigkeiten auf.
Kleine testgetriebene Klassen ermöglichen das Fehlerhandling und verringern die Komplexität.
Die Symfony Config stellt eine einfache Konfiguration bereit.
Die Debug-Komponente hilft beim Testen.
Über das Framework können wir einfach die Bibliothek SwiftMailer integrieren und konfigurieren.
The Symfony Way
Um die Abhängigkeiten aufzulösen, können wir den von Symfony bereitgestellten Service Container
verwenden. Ein Service Container
ist ein einfaches PHP Objekt, das sich um die Instanziierung von Services kümmert. Es gibt mehrere Wege
um einen Service im Container zu registrieren. Für dieses
Beispiel wird die Konfiguration mittels YML-Dateien verwendet.
Dependency Injection Konfiguration
Jetzt haben wir die beiden Services im Container registriert. Ein Service-Objekt wird immer nur
konstruiert, wenn es auch gebraucht bzw. aufgerufen wird.
Ein weiterer Vorteil: ein Service muss immer nur ein mal instanziiert und bei jedem Aufruf an dieselbe
Instanz zurückgegeben werden. Das bringt zusätzliche Performance!
Wie oben bereits beschrieben, muss im Folgenden erst wieder der Test für die Klasse NewsletterManager
geschrieben werden.
Newsletter Manager Test
Wir stellen die Erwartung, dass die Methode send mindestens einmal aufgerufen wird.
Über Prophesize wird deshalb wieder das Verhalten der Mailer-Klasse vorhergesagt. Der einzige
Unterschied zu oben ist, dass
anstatt eines dummy-Objekts ein Stub-Objekt instanziiert wird. Ein Stub ist quasi eine Erweiterung des
Dummy-Objekts um eine Logikkomponente.
Danach kann der NewsletterManager testgetrieben implementiert werden.
Testgetriebener Newsletter-Manager
Der Test läuft! Die Verwendung des NewsletterManagers ist jetzt ganz einfach möglich.
Klasse Newsletter
Gerade jetzt in der Gegenüberstellung werden die Unterschiede deutlich.
Mit Hilfe des Symfony Service Containers werden Abhängigkeiten zwischen Klassen intelligent verwaltet.
UnitTests sind auch ohne Abhängigkeiten deutlich einfacher zu schreiben.
Verwendung des Newsletter-Managers
Jetzt haben wir zwar einen einfachen Newsletter-Manager, jedoch muss der Versand der E-Mail auch gestartet werden.
In den meisten Fällen ist das entweder ein Webseite-Abruf – der Symfony Controller wird aufgerufen – oder
wir starten den Versand über die Console – ein Command wird ausgeführt.
Dafür muss im Controller oder Command einfach der Service Container aufgerufen werden:
Symfony liest die Konfiguration aus und erstellt den NewsletterManager mit allen Abhängigkeiten.
Konfiguration E-Mail-Versand
Symfony kann aber noch mehr. Es werden Konfigurationsmöglichkeiten bereitgestellt, um weitere
Anforderungen abdecken zu können, wie z.B. dass:
im Test- & Entwicklungssystem keine E-Mails versendet werden sollen,
der Absender leicht konfigurierbar ist und
E-Mails über Gmail versendet werden können.
Dafür wird wieder das YML-Format verwendet:
Versand deaktivieren
Versand an Test-Empfänger
Versand über Gmail
Fazit
So einfach ist es, einen wiederverwendbaren NewsletterManager mit Symfony zu generieren. Dieses Beispiel
zeigt dabei nur eine kleine
Kostprobe der Möglichkeiten, die das Framework mitbringt.
Das Zusammenspiel von testgetriebener Entwicklung
und Symfony führt fast zwangsläufig zu einem sauberen, leicht erweiterbaren, wartbaren
und wiederverwendbaren Code.