Mockery einrichten und mit PHPUnit nutzen
26. Juni 2013 Softwaretest von Eric Kubenka
Bereits letzte Woche habe ich kurz erklärt, wie sich PHPUnit einfach und schnell mittels Composer in Projekte integrieren lässt. Anschließend bin ich ausführlich auf die ersten Schritte im Test-Driven-Development eingegangen. Doch so leicht wie die Prozesse dort anhand eines einfachen Beispiels dargestellt wurden, gestaltet sich das ganze in der Realität selten.
Oft kommt es vor, dass Objekte als Abhängigkeiten verwendet werden müssen. Da es beim Unit-Testing aber nur darauf ankommt die aktuell zu testende Einheit zu testen, ist es nötig diese Abhängigkeiten durch Mocks, auch Dummy-Objekte genannt, zu ersetzen. Ein gutes Framework dafür ist Mockery.
Warum Abhängigkeiten "mocken"?
Die Begründung für das Ersetzen der benötigten Abhängigkeiten durch Mocks ist recht simpel. Ich erkläre es kurz am Beispiel. Angenommen die Methode logToFile()der Klasse Logbook soll getestet werden. logToFile() greift auf put() der Klasse Filesystem zurück. Nun soll aber nur sichergestellt werden, dass logToFile() richtig auf die Ergebnisse der put()-Methode reagiert und nicht, dass die put()-Methode erfolgreich in eine Datei schreibt. Dafür hat die Klasse Filesystem und die Methode put() ihre eigenen Tests, welche sicherstellen, dass das Dateisystem richtig angesprochen wird.
Durch mocken der Filesystem-Klasse und der Methode put() wird nun der logToFile()-Methode das Ergebnis von put() simuliert dargestellt. Das würde mit Mockery wie folgt aussehen.
<?php public function testLogToFileWritesToFile() { // mock that class $mockedFilesystem = Mockery::mock('Filesystem'); // mock the method and define which behaviour is expected $mockedFilesystem->shoudlReceive('put') ->with(Tthis is a line', 'logfile.txt') ->once() ->andReturn(true); // instantiate the class to test with the mocked version $log = new Logbook($mockedFilesystem); // assert that the write was successfull $this->assertTrue($log->logToFile('This is a line')); }
Damit bin ich nun wohl etwas mit der Tür ins Haus gefallen, jedoch habt ihr nun ein Bild von der ganzen Sache. Von nun an geht es Schritt-für-Schritt. Versprochen.
Mockery installieren
Um Mockery verwenden zu können, sollte das Framework zuerst eingebunden werden. Das geht mittels Composer kinderleicht wie folgt.
"require-dev": { "phpunit/phpunit": "3.7.*", "mockery/mockery": "dev-master" }, "autoload": { "classmap": [ "src" ] }, "minimum-stability": "dev"
Die Aufgabe
Um das Beispiel von oben wieder aufzunehmen, besteht die folgende Aufgabe in diesem Beitrag darin die logToFile()-Methode darauf hinzu prüfen, dass ein Schreibvorgang korrekt verarbeitet wird. Es ist lediglich zu testen, dass logToFile() richtig auf die Ausgänge der verwendeten Methoden reagiert und nicht dass die aufgerufenen Abhängigkeiten ebenfalls funktionieren. Beispielsweise wurde die verwendete Klasse oder Methode noch gar nicht fertig gestellt und damit würde der Test ohne mocken so lang fehlschlagen, wie die Abhängigkeiten nicht funktionieren, obwohl die eigentlich zu testende Methode korrekt arbeitet. Es sollen Unit-Tests sein, keine Integrationstests.
Das normale Vorgehen ohne Mocken
Selbst wenn man die Abhängigkeiten nicht mockt, sollte man darauf achten, dass man mit Dependency-Injection arbeitet. Bitte was?!. Dependency-Injection heißt das die Abhängigkeiten der Methoden oder Klasse an diese übergeben werden sollen und nicht erst in den aufgerufenen Methoden instanziiert werden.
Hinweis: Folgend werde ich aufgrund des Beispiels die Testklasse, sowie die zu testenden Klassen in einer Datei ablegen. Am Ende dieses Beitrags steht die komplette Datei zur Verfügung. Der Code steht wie immer auf GitHub bereit.
<?php // file: mockeryfirststeps.php - Example Dependency Injection class Filesystem { function __construct() { // see how this class not implement a put()-method? // but we're able to test this all classes that are calling for a put() method with mockery } } class Logbook { protected $file; // without dependency injection function __construct() { // ... you would'nt be able to test without touching the Filesystem-Class directly $this->file = new Filesystem; } // with dependency injection function __construct(Filesystem $file = null) { // you are able to replace the $file object with a mocked class $this->file = $file ?: new Filesystem; } } class LogBookTest extends PHPUnit_Framework_TestCase { public function testLogToFileWritesToFile() { // test goes here. } }
Mithilfe von Dependency-Injection ist es nun möglich das zu übergebene Objekt einfach durch einen Dummy zu ersetzen. Ohne Mocken sähe das so aus.
<?php public function testLogToFileWritesToFile() { // inject the dependency via constructor $log = new Logbook(new Filesystem); $log->logToFile('This is a line'); }
Ja, aber dann reicht doch auch Dependency-Injection oder?
Um komplett isoliert zu testen reicht das "Injektieren" von Abhängigkeiten allein nicht aus, da so immer noch beim Aufruf von logToFile() versucht wird mittels put() physisch auf das Dateisystem zu schreiben. Doch Testen in Isolation heißt eben, dass genau das alles nicht interessiert. Damit die Methode put() nicht wirklich auf das Dateisystem schreibt, wird diese einfach gemockt.
<?php class LogbookTest extends PHPUnit_Framework_TestCase { public function tearDown() { Mockery::close(); } public function testLogToFileWritesToFile() { $file = Mockery::mock('Filesystem'); $file->shouldReceive('put') ->with('This is a line', 'logfile.txt') ->once() ->andReturn(true); $log = new Logbook($file); $this->assertTrue($log->logToFile('This is a line')); } }
Folgend möchte ich kurz die dargestellten Aufrufe erläutern. Zuerst wird $file mittels Mockery ein gemocktes Objekt von Filesystem zugewiesen. Anschließend wird mittels shouldReceive() festgelegt, dass die Methode put() ein mal (once()) mit den Parametern 'This is a line' und dem Namen der Logdatei aufgerufen werden soll. Wenn dem so ist, dann soll die Methode true zurückgeben. Das ganze soll ausgelöst werden, wenn die Methode logToFile() der Klasse Logbook aufgerufen wird. Abschließend wird geprüft ob das Ergebnis von logToFile() den Wert true zurückgibt – also richtig und korrekt auf den Ausgabewert von put() reagiert.
Wichtig: Bitte stellt immer sicher, dass die Methode Mockery::close() zum Abschluss des Tests ausgeführt wird. Dazu biete es sich an die Methode wie dargestellt in der tearDown()-Methode an letzter Stelle aufzurufen.
Weiterhin versichert Euch, dass eine phpunit.xml-Datei im root-Verzeichnis hinterlegt ist. Wie diese aussehen kann, seht ihr hier.
Folgendes Bild zeigt sich nach Ausführen der des Tests.
Die Methode logToFile() existiert nicht. Schon vergessen? Wie arbeiten nach dem Test-Driven-Development-Entwicklungsmodell, also teilt PHPUnit an dieser Stelle mit, welcher Schritt als nächstes folgt. Daher wird folgende Methode in der Klasse Logbook implementiert. Denkt daran: "Keep the things simple".
<?php // class: Logbook public function logToFile($line) { return true; }
Nach Ausführung von dem bereits geschriebenen Test hängt sich nun zum ersten mal Mockery dazwischen.
Wie zu sehen ist, wirft Mockery selbst Exceptions um so zu verdeutlichen, dass die Erwartung des einmaligen Aufrufs von put() nicht erfüllt worden ist, da put() nicht ein einziges mal aufgerufen wurde. Der Test schlägt also fehlt, obwohl die Überprüfung auf true (assertTrue()) erfolgreich war und so alle Assertions des Tests korrekt waren. logToFile() wird nun vollständig implementiert.
<?php // class: Logbook public function logtoFile($line) { return $this->file->put($line, 'logfile.txt'); }
Nach erneutem Ausführen des Tests, wird dieser nun erfolgreich durchlaufen, obwohl nicht ein einziges Mal versucht wurde auf das wirkliche Dateisystem zu schreiben.
Abschluss
Ich hoffe, dass die Thematik halbwegs verständlich erklärt wurde und Euch nun deutlich geworden ist, wann es halbwegs sinnvoll ist Klassen und Abhängigkeiten zu mocken. Natürlich ist dies nur ein kleiner Ausschnitt der Möglichkeiten von Mockery. Alles weitere steht in diversen Dokumentationen. Ebenfalls kann ich Euch folgenden Artikel von Patkos Csaba empfehlen.
SourceCode: Siehe Github