Gestern hab ich mich etwas darüber ausgelassen, wie doof doch einige Code Koppelungen von symfony mir das Testen behinderten.
Vor allem drei Dinge haben mir das Leben schwer gemacht: die Konfiguration, sfContext und Datenbankabhängigkeiten. Und ich habe für alle drei auch Antworten finden können. Vielleicht nicht die endgültigen, aber schon sehr akzeptable, wie ich meine.
Vielleicht sag ich erstmal, was überhaupt meine Probleme waren.
Zum einen ist es nicht immer ganz leicht herauszufinden, was man nun alles initialisieren muss, um eine Klasse zu Testen. Und für die Dinge, die initialisiert werden sollen, ist es nicht immer ganz einfach herauszufinden, was man dazu alles hochfahren und welche Konfiguration geladen werden muss.
Das führte dazu, dass ein Test alleine durchlaufen konnte, aber es in einer Test Suite nicht mehr tat. Und wenn man ihn da zum Laufen bekommen hat, konnte er immer noch auf dem Continuous Integration Server fehlschlagen.
Ein anderes Problem war die Ausführungszeit der Tests vor allem auf dem CI Server. Spitzenzeiten von bis zu 5 Minuten finde ich mehr als grenzwertig, wenn man sich mal den Umfang meines Projektes vergegenwärtigt (ca. 13 überschaubare Klassen inklusive der Controller).
Unterm Strich waren meine Tests alle zusammen wenig stabil und zu langsam.
Als Grund für die lange Ausführungszeit konnte ich ganz klar den Bootstrap von symfony ausmachen. Das ständige Hochfahren des Frameworks ging extrem auf die Performance.
Bei dieser Testdauer Übersicht kann man schon ahnen, wo die Probleme liegen. Hier mal die teuren vier:
- sfImageTransformExtraPluginConfigurationTest testet die Plugin Configuration, die bereits alle anderen Configs benötigt, um überhaupt richtig instanziiert werden zu können
- sfImageSourceTemplateTest testet ein Doctrine Behaviour, welches ein Doctrine_Record benötigt, um die einzelnen Methoden aufrufen zu können
- sfImageTransformManagerTest ist tatsächlich nicht ganz sauber entworfen.. (Das habe ich aber gestern bereits korrigiert!)
- sfImageSourceDoctrineTest testet einen Stream Wrapper, der eine Resource zurückliefert, die er mit Hilfe der Datenbank bestimmt
Mittlerweile sieht die gleiche Übersicht wie folgt aus:
Das kann sich schon eher sehen lassen. Eine Ausführzeit von 6 Sekunden ist auf jeden Fall akzeptabel, was ja nicht nur für den CI Server sondern vor allem für die manuelle Ausführung während der Entwicklung kritisch ist.
Aber was habe ich gemacht?
Separation des Plugins
Unbedingt notwendig für eine höhere Stabilität der Tests unter verschiedenen Umgebungen (Entwicklungsumgebung und Continuous Integration Server) war es das zu testende Plugin aus dem Projekt zu lösen. Das heisst:
- Eine separate Entwicklungsumgebung. Um zu vermeiden, dass man doch Projekt-Abhängigkeiten einbaut und mittestet, sollte die Entwicklung des Plugins separat stattfinden oder zumindest das Testen.
- Ein separater PHPUnit Aufruf. Damit bekommt man auch Konflikte wie gleich benannte Klassen in den Griff. Überhaupt besteht im Grunde genommen kein Grund in einem Projekt auch alle darin genutzten Plugins zu testen, denn diese sollen ja vor allem Projekt-unabhängig funktionieren.
- Ein separater CI Job. Zu finden unter http://automat.ical.ly/job/sfImageTransformExtraPlugin/
- Ein separates Repository. Das macht es dem CI Server einfacher, einfach Subversion nach updates zu befragen, ohne dass Plugin und Projekt Updates sich gegenseitig ins Testing schiessen.
Als “Entwicklungsumgebung” reicht es mir aus, das Plugin irgendwo ohne symfony Projekt abzulegen und lediglich die symfony Bibliotheken bereitzustellen (export SYMFONY=…./symfony/lib). Sobald kein Projekt mehr vorhanden ist und die Tests trotzdem durchlaufen, kann man sicher sein, dass es keine Abhängigkeiten gibt.
Der separate PHPUnit Aufruf hat zwei Vorteile. Zum einen umgeht es den Namenskonflikt, über den ich vorgestern gestolpert bin. Zum anderen werde ich solche Aufrufe nur noch direkt aus dem Plugin Basisverzeichnis ausführen. Denn sobald ich eine phpunit.xml anlege, weiss ich, dass die darin enthaltenen Pfade relativ zum Plugin sind.
Ein separater CI Job macht ebenfalls total Sinn, denn dann kann ich dort separat konfigurieren und bringe nicht eventuelle Projekt-Abhängigkeiten in die Testumgebung.
Das separate Repository ist erstmal optional, aber sicher sinnig, wenn ich dieses Plugin auch in anderen Projekten nutzen will.
Doctrine Verbindung mocken
Die Doctrine Datenbank Verbindung kostet Zeit und ist bisher aber gar nicht wirklich nötig, weil ich gar keine Modelle im Projekt habe, sondern lediglich mit Modellen aus dem Projekt zusammenarbeiten will.
Eine DSN habe ich wie folgt angegeben, aber obwohl SQLite für Testzwecke sicher besser ist, als MySQL o.ä. ist die Auswirkung auf die Performance recht gross.
all:
doctrine:
class: sfDoctrineDatabase
param:
dsn: sqlite:////fixture.db
Deshalb habe ich in den setUp() Methoden der TestCases, die irgendwie mit Doctrine arbeiten folgenden Schnipsel eingebaut:
$this->dbh = new Doctrine_Adapter_Mock('mysql');
$this->conn = Doctrine_Manager::getInstance()->openConnection($this->dbh, 'mysql', true);
Das true ist wichig, denn es aktiviert auch gleich die so übergebene Verbindung.
Im Ergebnis wird die fixtures.db gar nicht mehr angelegt. Die DSN wird lediglich zum initialisieren benötigt, Doctrine danach aber wieder umkonfiguriert.
Im Sourcecode von Doctrine habe ich auch entdeckt, dass man eine DSN mit dem mock:// Protokoll angeben kann, aber das habe ich noch nicht zum Laufen bekommen. Sollte ich das noch hinbekommen, könnte ich mir aber sogar den Schnipsel sparen.
Ein Doctrine_Record mocken
Wenn ich keine eigenen Modelle habe und mich auch nicht Daten aus irgendeiner Datenbank bedienen möchte (und darf!), dann brauche ich ein Mock-Doctrine_Record und eine dazugehörige Mock-Doctrine_Table.
Das require() ich einfach überall, wo ich sowas benötige und implementiere dort alles, was ich von diesem Record in Tests benötige. Ich muss allerdings aufpassen, nichts Test-kritisches zu mocken, was eventuell zu einem allways-pass führt. Nur Sachen mocken, um Abhängigkeiten aufzulösen!
sfContext nicht öfter instanziieren als notwendig
Ich habe gelernt, dass die setUp() Methode in einem TestCase mit dessen Instanziierung ausgeführt wird. Damit hatte ich angenommen, dass sie einmal vor allen im TestCase enthaltenen Test-Methoden läuft. Das stimmt aber so nicht.
Ein TestCase wird für jede in ihm enthaltene Test Methode erneut instanziiert!
Wenn ich dann in der setUp() Methode ein sfContext Objekt erzeuge, dann mache ich das erneut pro Test Methode. Doof.
Folgender Schnipsel hilft da gewaltig:
if(!sfContext::hasInstance('frontend'))
{
sfContext::createInstance($this->projectConfiguration->getApplicationConfiguration('frontend', 'test', true));
}
So wird die Instanz nur noch erzeugt, wenn es noch keine gibt.
Ich werde mir meine TestCases immer wieder ansehen. Sollte ich dabei feststellen, dass ich diesen Schnipsel mehr als nur wenige Male verwende, dann werde ich ihn wohl in die setUp() Methode der TestSuite umziehen, die kann dann für alle Tests was vorbereiten.
So, ich bin erstmal zufrieden mit mir. Der Release Tag kommt näher!
Continuous Integration · Hudson · phpUnit · sfImageTransformExtraPlugin · symfony · Tests · Unit Tests
-
http://blog.nevalon.de Gordon Franke
-
http://www.robo47.net/ robo47
-
http://www.robo47.net/ robo47
<< Mit phpDocumentor eine vernünftige API Dokumentation für ein symfony Plugin erstellen



