Wer schon mal ein System gepatcht hat und dann die Seite nur noch 500 sagen gesehen hat, wünscht sich, der Fehler wäre vorher aufgefallen. Also müssen jetzt sofort UnitTests her. Aber wo anfangen? Und wie bekomme ich nur die Kopplungen zur DB, zum Solr oder sonstigen Komponenten weg?
OK, mal schön der Reihe nach:
Schritt 1: Grundstruktur
1 2 3 4 5 6 7 8 9 10 11 |
namespace Test; abstract class DbSeeder extends \PHPUnit_Framework_TestCase { protected function setUp() { parent::setUp(); // hier werden wir später unsere db verbindung simulieren } } |
1 2 3 4 5 6 7 8 9 |
namespace Test; class SearchTest extends DbSeeder { public function testInput() { // user erster test } } |
Wir führen hier zunächst eine Zwischenschicht ein, die für uns die Datenbankarbeit übernehmen wird.
Schritt 2: Datenbank mocken
Um unseren ersten Test durchführen zu können müssen wir jetzt die Datenbank simulieren. Die muss erst mal überhaupt nicht existieren, wir wollen einfach nur prüfen ob unsere zu testende Funktion auch die richtigen Operationen durchführt.
1 2 3 4 5 6 7 8 9 10 |
public function testExample() { // Datenbank Adapter mocken $db = $this->getMock('\PDO', [], [], '', false, false); $db->expects($this->once())->method('query')->with($this->equalTo('INSERT INTO testtable VALUES(null, 10, 20, 30)')); // Instanz der Klasse die wir testen wollen $class = new \ClassToTest($db); $class->insertSomeDataInDb(); } |
Ein wichtiger Teil der UnitTests sind sogenannte Mock-Objects. Damit erhalten wir die Kontrolle über Klassen, die von unserem Testsubjekt benötigt werden. Wir verwenden hier ganz allgemein PDO, aber das geht natürlich mit jeder anderen Klasse auch. Durch die beiden „false“ am Ende verhindern wir, dass der Original-Konstruktor von PDO aufgerufen wird. Damit benötigen wir keine real existierende DB für unseren Test mehr.
Irgendwie muss natürlich unsere zu testende Klasse die simulierte DB auch verwenden. Wer mit einem Framework arbeitet wird sich da leicht tun. Die DB ist irgendwo als Service definiert und wir automatisch „injected“. Es muss also lediglich dieser Service überschrieben werden. Wer global eine Datenbankverbindung hat und immer mit „global $db;“ arbeiten muss, kann das hier einfach genauso tun.
Der Rest muss die Datenbank in die Klasse reichen wie im Beispiel oben. Dazu sind dann vermutlich Anpassungen an der Klasse notwendig.
Schritt 3: Test durchführen
Der eigentliche Test passiert hier:
1 |
$db->expects($this->once())->method('query')->with($this->equalTo('INSERT INTO testtable VALUES(null, 10, 20)')); |
Hier sagen wir nun unserem Mock-Object was genau wir erwarten. Diese Erwartungen werden nach Testende von PHPUnit automatisch überprüft. Wir erwarten also folgendes: Die Funktion „query“ wird genau einmal aufgerufen „once()“ mit einem Parameter, der exakt unserem erwarteten SQL-Statement entspricht („equalTo()“).
Und schon haben wir unseren ersten Test ohne lästige DB erledigt.
Schritt 4: Weitere Möglichkeiten ausprobieren
Eine einfache Anwendung des Datenbank-Mocks haben wir jetzt gesehen, aber was haben wir denn da noch?
Andere Häufigkeiten
Zunächst kann natürlich eine anderen Häufigkeit erwartet werden. Es stehen zur Auswahl:
1 2 3 4 |
$this->any(); // beliebig oft $this->exactly(3); // genau drei mal $this->once(); // genau einmal; shortcut für exactly(1) $this->never(); // darf nicht aufgerufen werden; sinnvoll wenn unter bestimmten Bedingungen etwas nicht stattfinden soll |
Ein Sonderfall ist
1 |
$this->at(1) |
Damit können wir für verschiedene Aufrufe der selben Funktion unterschiedliche Erwartungen (zum Beispiel Parameter) definieren. Wichtig zu beachten ist, dass die Zählung der Aufrufe für die gesamte Klasse vorgenommen wird und nicht die einzelne Funktion.
Rückgabewerte
Natürlich erwarten unsere Testsubjekte auch Rückgaben von den DB-Funktionen. Auch hier bietet unser Mock die notwendigen Hilfsmittel.
1 |
$db->expects($this->any())->method('fetch')->will($this->returnValue(['1', '2', 'drei'])); |
Uns ist also erst mal egal wie oft fetch() aufgerufen wird und mit welchen Parametern das geschieht. Aber wir definieren einen Rückgabewert („returnValue“). Damit haben wir für unseren Test immer die gleichen Bedingungen und können überprüfen ob unser Testsubjekt auch immer das gleiche tut.
Andere Möglichkeiten sind:
1 2 3 4 5 |
$this->returnValue(42); // fester Wert $this->returnValueMap([42, 21, 84]); // fester Wert aber für jeden Aufruf der Funktion ein anderer $this->returnSelf(); // Wenn die gemockte Funktion normalerweise return $this; enthält (fluent interface) $this->returnArgument(1); // einen übergebenen Paramter (hier der erste) zurück geben $this->returnCallback(function($arg1){ return 'foo'; }); // callback aufrufen |
returnValueMap() liefert von uns definierte Werte solange der Vorrat reicht. Für nachfolgende Aufrufe wird immer wieder der letzte Wert zurück gegeben.
returnCallback() übergibt an den Callback die Parameter exakt so wie sie das Testsubjekt an die eigentliche Funktion übergibt. Hier können wir also auch die Übergabeparameter noch einmal eingängiger testen als es mit ->with($this->equalTo()) möglich wäre. Des Weiteren kann die Rückgabe von den Parametern abhängig gemacht werden.
Ein Gedanke zu „PHP und UnitTests wie fange ich an?“
Kommentare sind geschlossen.