Unittests ohne Datenbank

Dominik [php, Datenbanken, Qualitätssicherung]

In der Vergangenheit habe ich schon bei einigen Projekten Unittests gesehen, welche mit einer Datenbank gearbeitet haben. Die Probleme dabei waren immer dieselben, wenn

  • die Datenbankstruktur geändert hat
  • die Daten nicht korrekt sind
  • die Reihenfolge nicht stimmt

funktionierten mindestens einige Tests nicht mehr. Damit einher ging auch ein grosser Konfigurationsaufwand auf der Entwicklermaschine. Dabei musste oft nicht nur der Connectionstring angepasst werden. Die Tests waren selten stabil und so versandeten die guten Vorsätze schnell wieder, weil keiner die Nerven dazu hatte. Dazu geht das Restoren einer grösseren Datenbank auch nicht gerade schnell. Die Abarbeitung einiger hundert Tests ging so schon mal über 10 Minuten.

Alles wird gut

Nun sollte man ja bekanntlich aus seinen Fehlern lernen! Mit anderen Worten: Finger weg von Datenbanken in Unittest Umgebungen. Nur wie stellen wir das an?

Als erstes muss der Aufbau der Applikation für Unittests konzipiert worden sein. Ein bestehendes Projekt so umzubauen geht nicht immer. Wenn wir aber mehr oder weniger auf der grünen Wiese mit einem MVC Projekt starten, geht das alles relativ einfach. Hier gehen wir nicht auf das MVC Pattern ein, das ist ein anderes Thema.

Es wird gemockt

Wir nehmen hier als Beispiel ein Repository für den Datenzugriff auf Produkte. Dazu gibt es folgendes (vereinfachtes) Interface.

interface IProductRepository {
  /**
   * Gibt ein Array mit den Produkten einer Kategorie zurück
   */
  function getProductsByCategory($categoryId);
  /**
   * Gibt ein Array mit den Produktdaten zurück
   */
  function getProductById($id);
}

Wie und woher die Daten geladen werden interessiert uns hier nicht, wir möchten die Datenbank ja loswerden... Im Unittest können wir dieses Interface verwenden, um ein Mockobjekt davon zu erzeugen.

public function testProductList() {
  // Erstellen des Mockobjekts anhand des Interfaces
  $repository = new Mock("IProductRepository");
 
  // Konfiguration der Methoden
  $repository->expects($this->exactly(1))
    ->method('getProductsByCategory')
    ->with($this->isType("integer"))
    ->will(array("p1", "p2"));
  $controller = new ProductController($repository);
 
  // Code-Aufruf, in welchem getProductsByCategory aufgerufen wird.
  $result = $controller->Category(2);
 
  // Überprüfen ob die Rückgabe korrekt ist
  $this->assertEquals(2, count($products));
}

Mit der expect-Methode können wir nicht nur definieren, wie sich der Mock verhält sondern auch überprüfen ob dies so war. Der Aufruf erwartet zuerst eine Matcher Instanz. Wir definieren, dass diese genau einmal aufgerufen werden darf ($this->exactly(1) oder $this->once()). Danach sind die Parameter dran, wir haben einen Parameter vom Typen integer ($this->isType("integer")), mehrere Parameter werden einfach Komma-getrennt übergeben. Falls es darum geht, auf den genauen Wert zu prüfen kann dies mit ($this->identical(5)) erreicht werden. Als letztes ist noch die Rückgabe zu definieren, welche der Methodenaufruf hat. ->will(array(1,2)) bewirkt dies.

Fazit

Mit dem Mockingframework von phpUnit haben wir also sehr genau unter Kontrolle, was wo aufgerufen wird und welche Rückgabe wir davon erhalten. So ist eine Datenbank für die Unittests entgültig nicht mehr nötig. Wer dazu ein MVC Framework verwendet hat auch die technische Grundlage testbaren Code zu schreiben. Wir setzen dazu auf unser hauseigenes FADAL mvc. Das Ziel dieses Jahres ist es, die Dokumentation dazu fertigzustellen und das Framework als OpenSource herauszugeben.

zurück