PHP Blogger

Startseite Schreib mir ne Mail! RSS Abo Webnews

PHPUnit - Wo soll man anfangen

Nachdem es ja in meinem letzten Beitrag doch zahlreiche gute Beiträge in den Kommentaren gab und mich das Thema nicht losgelassen hat, will ich mal versuchen meine Erkenntnisse zusammenzufassen und eine neue Diskussion anzustoßen. Ich denke, dass ich meinen Prozess während ich mich mit Unit-Testing weiter auseinandersetze in einigen Beiträgen festhalten werden. Deshalb jetzt die ersten Gedanken zum Thema, bevor ich noch einen Test geschrieben habe.

Bisher war es für mich immer schwer das Konzept von Unit-Tests zu verstehen, weil ich das Gefühl habe, dass ich die Konzepte, die einem nahe gelegt werden nicht auf meinen bestehenden Code übertragen kann. Dazu ist zu sagen, dass man zuerst liest, dass es sowieso sehr schwer ist Tests nachträglich für bestehenden Code zu bauen, aber das sollte mich jetzt nicht davon abhalten es zu probieren, oder?

Unit-Tests sind dafür da Einheiten des Codes zu testen. Eben die Units oder Methoden oder Funktionen oder auch ganze Klassen, aber dann ebenfalls auf Methoden-Ebene. Ein Unit-Test soll sicherstellen, dass eine Methode mit gegebenen Parametern auch das entsprechende Verhalten zeigt. Und ist dann besonders nützlich, wenn man den Code oft verändert oder eben viele Leute an dem Code arbeiten.

Da kommt dann auch der Begriff des Test-Driven-Development zum tragen. Das Paradigma sieht vor, dass man sich überlegt, dass man eine bestimmte Klasse benötigt, die verschiedene Methoden besitzt, aber bevor man jetzt die Klasse implementiert, wird zunächst der Test dafür geschrieben. Der sicherstellen soll, dass die Methoden dann auch das gewünschte Verhalten zeigen. Soweit so gut.

Nur habe ich jetzt schon eine Applikation mit einer Menge Klassen und einer Menge Methoden. Und noch keine große Erfahrung mit Unit-Tests. Klar habe ich schon mal gehört, was man damit macht und auch schon mal einen Test geschrieben, aber eben an so Beispielen die auch beim Unit-Testing rangezogen werden. Eine Kontoklasse zum Beispiel.

Aber ich habe jetzt eine Webapplikation, die DB Anbindungen hat, Konfigurationsobjekte, die gemeinsame Aufgaben übernehmen etc. Deshalb werde ich mir eine relativ kleine Klasse suchen, die möglichst wenig Abhängigkeiten hat und trotzdem ein paar Funktionen im System erfüllt.

Damit ich aber nicht bei Null anfangen muss mit meinen Tests möchte ich noch eine kleine Funktion zeigen, die ich gerade programmiert habe:

function _logCall($p_args, $p_class, $p_method) {

	$method = new ReflectionMethod($p_class, $p_method);

	$log = "\r\n\r\n";

	foreach($method->getParameters() as $i => $param) {
		$log .= $param->getName() . ' => ' . $p_args[$i];
	}

	if(!is_dir('logs/' . $p_class)) {
            mkdir('logs/' . $p_class);
	}

	file_put_contents('logs/' . $p_class . '/' . $p_method, $log, FILE_APPEND);
}

Aufgerufen dann mit:

_logCall(func_get_args(), __CLASS__, __FUNCTION__);

schreibe ich damit schöne Logfiles in denen der entsprechende Methodenaufruf mit Parametern, die auch im Live-Betrieb vorkommen erstellt wird. Daraus kann ich dann nutzen ziehen und entsprechende Testfälle bauen. Das ist zumindest die Idee.

Ähnliche Artikel:

  1. PHP Mailer Update: miniMail Version 1.3.2
  2. PHP Datum Klasse
  3. Email Winzling

timi meint dazu:

5. Dezember 2008 um 08:33

Hey Phil, coole Idee, Live-Parameter fürs UnitTesten zu protokollieren! Daraus könnte man einen Pool in einer Datenbank zusammenstellen und daraus teilweise automatisiert die UnitTestaufrufe generieren…

Christoph meint dazu:

5. Dezember 2008 um 12:06

Eigentlich soll man genau das ja nicht machen, weil man auf diese Art nur typische Parameter bekommt. Wenn man jetzt aber einen Hacker hat oder eine blöde Verkettung von Umständen, kann es auch mal passieren, dass du statt eines Objects mal NULL bekommst. Oder mal eine negative Zahl statt einer positiven.

Diese Fälle würdest du durch Sammeln von Live-Parametern ja gar nicht auffangen.

timi meint dazu:

5. Dezember 2008 um 13:48

Da hast Du sicherlich Recht, Christoph - NUR auf Live-Parameter sollte man sich nicht verlassen. Zumal das Protokollieren von Objekten und Querverweisen sicherlich schwer fällt.

@Phil: Wie willst Du ganze Objekte, die als Parameter übergeben werden, protokollieren? Hast Du Dir da schon was überlegt?

phil meint dazu:

5. Dezember 2008 um 18:44

Hi Jungs,

ja, ich habe da noch ein paar weitere Convenience Funktionen, die ich noch in den Artikel hinzufügen kann. Zum einen kannst du für deine eigenen Objekte eine ->_toString() Methode zur Verfügung stellen und für PHP-Interne Objekte kann man sicher ein var_export($p_args[$i], true) einsetzen.

@Christoph
Du hast schon recht, dass man damit nicht alles abfangen kann, aber das kann fast nur passieren, in Funktionen, die auf externe Eingaben warten, also ist die Anzahl der Funktionen schon mal reduziert und zum anderen will ich das ja auch benutzen, um einen Anfang hinzubekommen. Vom bloßen ansehen einer existierenden Funktion komme ich nämlich vielleicht noch nicht mal auf alle Kombinationen, die mittlerweile im Betrieb vorkommen und so hat man schon mal eine Sammlung, die man nutzen kann.

Und noch etwas weitergesponnen, wenn man dann ein Set von Testfällen hat, dann könnte man auch das anlegen von neuen Testfällen automatisieren, so wie Timi das vorschlägt.

Dirk meint dazu:

8. Dezember 2008 um 06:52

Meiner Ansicht nach ist das etwas quer gedacht. Unit Testing hat in meinen Augen nichts mit dem Testen realistischer Fälle zu tun, sondern soll die Funktionalität einzelner Bausteine sicher stellen.

Ziel von Test Driven Development ist doch, erst Tests zu schreiben und dann eine Funktion oder Methode, welche den Test erfüllt. Anschließend Refactoring unter der Prämisse, dass der Test nach wie vor positiv durchläuft.

Es geht also darum die Kernaufgabe einer Methode zu erfassen und genau diese zu testen. Der Test ist die Code gewordene User Story bzw. Anforderung. Und da ist das Problem: automatisiete Tests hast du ebenso wenig unter Kontrolle, wie eine ungetestete Methode, weil sie nicht von einer Anforderung abgeleitet ist, sondern von einem Ablauf in der Anwendung, der ich (je nach Anforderung) jederzeit ändern kann. Das ist keine Anforderung, das ist ein Ablauf.

Als Tipp: es hilft, wenn du Unit Tests bei bestehenden Systemen von “Innen nach Außen” baust. Suche dir erst die Klassen, die wirklich im Kern deines Systems stecken und keine Abhängigkeiten haben (meistens Datenbank, Debugging etc). Fange mit deren Methoden an und hangel dich langsam nach außen. Es bringt nichts von außen anzufangen, wo Fehler entweder an er Methode liegen oder an einer darunterlegenden Abhängigkeit.

phil meint dazu:

8. Dezember 2008 um 06:57

Hey Dirk,

das ist schon richtig. Aber die innersten Bausteine meine Applikation sind alle “protected” oder “private”?! Fällt dir da irgendwas auf?! Im Bezug auf Unit-Testing…

Das ist übrigens gleich die erste Hürde, die ich gefunden habe, als ich anfangen wollte meine “Bausteine” zu suchen…

Viele Grüße, der noch nicht aufgebende
Philipp

Dirk meint dazu:

9. Dezember 2008 um 10:36

Hm.

Mein Klassen-Design hat nicht viele Klassen, deren Methoden ausschließlich Private bzw. Protected sind. Der einzige Nutzen für so Klassen wäre natürlich sie durch andere abzuleiten und dann kann ich wiederum diese testen.

Sprich: irgendwo ist immer die innerste Klasse, die anfängt zu arbeiten. Alles andere sind Helper-Klassen, welche die Grundlage für die Worker-Klassen bilden.

Nach meiner Interpretation von Unit Testing brauche ich Helper-Klassen nicht zu testen. Ich teste nur gegen Anforderungen und es gibt keine Anforderungen für Helper-Klassen, wenn ich keinen Call darauf machen kann. Wenn sie Worker-Klassen unterstützen sind sie für mich nur eine Verlängerung der Worker-Klasse und müssen als Gesamtes den Test erfüllen.

Etwas hergeholt, funktioniert in meinem Design aber ganz gut. Wie gesagt versuche ich reine Helper-Klassen weitestgehend zu vermeiden.

Dirk meint dazu:

9. Dezember 2008 um 10:50

Warte mal - sehe ich das im Impressum richtig, dass du aus Darmstadt kommst? Wenn du Zeit und Lust hast können wir uns gerne mal auf einen Kaffee zusammen setzen.

Wolfgang Stengel meint dazu:

9. Dezember 2008 um 15:39

Was schon immer mein Problem bei Unit Testing war sind Funktionen die Abhängigkeiten haben, weil sie z.B. etwas an der Datenbank ändern, oder auf die Session zugreifen. Der Rückgabewert hängt also nicht nur von den Eingabeparametern ab. Viele Funktionen haben auch garkeinen Rückgabewert, weil sie nur etwas “machen”, und nicht etwas “bereitstellen”. Ist das nur bei mir so oder trifft das immer auf die meisten Funktionen zu?

phil meint dazu:

9. Dezember 2008 um 17:53

@Dirk:
Da komme ich ursprünglich aus der Nähe her, bin aber im Augenblick eher 8000km davon entfernt :)

Bei mir sind die Methoden protected, das hat nichts mit ableiten zu tun, sondern mit Interface-Design. Ich möchte nicht, dass bestimmte Funktionen von anderen Objekten aufgerufen werden können. Vielleicht sind das auch im weitesten Sinne Helfer-Klassen, aber dann ist das Testen schon wieder über die von Wolfgang ebenfalls festgestellten Abhängigkeiten verteilt.

Bsp: Meine Datenbankabstraktion. Ich habe ein Singletonobjekt, welches die DB Connection herstellt und entprechend die Queries ausführt. Diese hat (ungefähr) folgende public functions:
- runSelect
- runInsert
- runUpdate
- …

Da ich aber gerne eine Abstraktion einführen wollte und nicht direkt das SQL in meine Logik schreiben wollte (vielleicht müsste man für eine andere DB ja etwas ein bisschen anders machen, z.b. heißt LIMIT nicht mehr LIMIT sondern RANGE)
wird eine query in der Form
array(’type’ => ‘SELECT’, ‘table’ => ‘myTable’, ‘cols’ => array(’a',’b'));
übergeben. Daraus baut dann meine Datenbankklasse
SELECT `a`, `b` FROM `myTable`
soweit so gut. Aber am liebsten würde ich ja die Funktion ‘buildCols’ direkt testen, weil die ist die, welche einen Input und einen Returnwert bekommt. Nur ich möchte eben nicht, dass irgendwer diese Funktion aufruft -> protected definiert… Damit kann aber auch der Unittest die Funktion nicht mehr aufrufen. Daher müsste ich die ‘runSelect’ Methode testen und dann kommen wieder die dicken Abhängkeiten mit DB Abfrage wird ausgeführt etc. zustande…

@Wolfgang
Genau das ist mein Problem mit dem ich mich gerade auseinandersetze. Es ist also nicht nur bei dir so. Die Ansicht der Tester zu dem Thema ist, dass dein Applikationsdesign deshalb nicht gut ist. Stattdessen würde man erwarten, dass man jede Menge Funktionen hat, die eben nur was zurückgeben und nicht den Zustand eines Objektes ändern…

Oder anderes Beispiel:
Ich habe eine Methode:
public function setIniVal($p_name, $p_value) {

if(!is_string($p_name) or empty($p_name)) return false;

if (strstr($p_name, ‘[]‘)) {
$p_name = str_replace(’[]‘, ”, $p_name);
$this->ini[$p_name][] = $p_value;
}
else {
$this->ini[$p_name] = $p_value;
}

return true;

}

Mit der ich auf zwei Art und Weisen einen Wert setzen kann, der von verschiedenen Klassen abgefragt wird und den Grundzustand meiner Applikation festlegt (Bsp. welches ist die gerade aufgerufene Seite, was ich nicht jedes Mal neu ‘berechnen’ lassen möchte, sondern einmal festlege und dann nur die Value abfrage. Der Parameter ‘ini’ ist ebenfalls als protected definiert. Nur für sich gesehen kann ich an der Methode nur testen, ob ich “true” oder “false” als Rückgabewert bekomme. Und selbst wenn ich die “getIniVal” Methode nehme, um zu testen, ob der richtige Wert gesetzt wurde, kann ich nicht, wie in dem tollen Array Bsp. auf der phpunit-Seite testen, ob ich wirklich nur einen einzelnen Wert gesetzt habe. Muss ich das dann nicht testen, weil meine Anforderung an die Methode einfach nur ist: Setze einen Wert auf eine bestimmte Art und Weise und die Anforderung an die get Methode ist: Liefere mir einen Wert zu einem Schlüssel? Sind das dann die einzigen Testfälle, die ihr dazu bauen würdet?!

Lars Schultz meint dazu:

12. Dezember 2008 um 09:03

Hallo Phil,

@off topic
deine _logCall Funktion hat mich an meine debugCall() Funktion erinnert, die ähnliches Bewirkt wie deine, aber etwas komfortabler zu nutzen ist;)

[Code]
function debugCall($msg){
$trace = debug_backtrace();
array_pop($trace); //skip debugCall() call
$call = array_pop($trace); //this is the context where debugCall() was called

echo ”. $call['class'] .’::’.$call['function'] .’(’.$call['line'].’): ‘. $msg;
}

debugCall(’lars war hier’);
[/Code]

Natürlich hab ich mir dazu nicht nur diese Funktion geschrieben, sondern gleich ein ganzes Set von Debugging-Funktionen gemacht…unter anderem stacktrace()…welches einen HTML-Freundlichen, gehighlighteten Output liefert, oder writeDebugCall() welches einen Aufruf in ein Logfile schreibt. Falls du Interesse hast, kann ich irgendwo ein Link auf meine Debug-Source veröffentlichen:)

@TDD
Du hast 50 Klassen ohne Tests?;) Ich hab 600! Versuch das Mal hinterher zu ändern. Das Problem mit der DB kenn ich und ist für mich auch eher schwierig zu umgehen, da meine Daten sehr komplex aufgebaut sind. Mein Ansatz dazu war: ich schreibe Tests für die Dinge (zur Zeit noch ohne PHPUnit oder einem sonstigen Framework, die mag ich nich so…;) ), die sich als Bug-Anfällig erweisen, um dann nach Änderungen, deren Funktionalität bzw. geänderte Funktionalität nachvollziehen zu können.

Was ich mir für die Zukunft überlegt habe ist eine Art Test, der die Daten Schritt für Schritt aufbaut, und jeweils die Daten überprüft. Dazu könnten nach jedem als Erfolgreich erachteten Schritt snapshots einer DB gemacht werden, um diese Dann zu vergleichen. SEHR AUFWÄNDIG!;) Mal sehen…vielleicht kann ich mich doch noch mit PHPUnit oder sowas anfreunden.

Sonst hat mir das Konzept “http://en.wikipedia.org/wiki/Separation_of_concerns” am meisten gebracht was die Test und Debugfähigkeit meines Codes anbelangt.

Motoröl meint dazu:

23. Dezember 2008 um 23:18

Super Beitrag zum Jahresende! Frohe Weihnachten und nen guten Rutsch in 2009

RSS für Kommentare zu diesem Artikel · TrackBack URI

Schreib Deine Meinung