Test Driven Development – Erklärung am Beispiel mit PHPUnit

19. Juni 2013 Softwaretest von Eric Kubenka

Während viele Außenstehende von PHP immer noch das Bild der zurückgebliebenen Sprache nach Wordpress-Code-Design haben, entwickelt sich die meist genutzte Programmiersprache langsam aber sicher, wie ich es sage, zum .NET-Klon.

Viele professionelle Projekte werden nach diversen Design-Pattern, wie MVC erstellt. Moderne PHP-Entwickler aber auch Beteiligte im Social Coding  greifen auf Softwaretests zurück, vor allem aber hält die testgetriebene Entwicklung nach und nach Einzug. Folgend möchte ich an einem Beispiel die Vorgehensweise beim TDD erläutern. Source-Code am Ende des Artikels.

phpunitrunfromcmd

 

Test-Driven-Development. Warum?

Test-Driven-Development ist das aufstrebende Entwicklungsmodell der PHP-Community. Mit bestimmt wird das Ganze durch Social-Coding-Plattformen wie GitHub. Keiner möchte eine bestehende Anwendung erweitern und anschließend daran schuld sein, dass nach dem Merge des Pull-Requests gar nichts mehr geht. Also erhält das Testen höchste Priorität.

TDD heißt vereinfacht, dass der Test vor dem zu testendem Code geschrieben wird und lässt sich ideal bei Unit-Tests umsetzen. Die Abfolge bei der Entwicklung einer neuen Methode ist also folgende:

  1. Einen fehlschlagenden Test entwickeln
  2. Den zu testenden Code schreiben – Der Test ist erfolgreich
  3. Refactoren des Tests und der zu testenden Komponente

Das ist schon alles. Für jede Klasse, jede Methode, jede Komponente, jede Unit werden diese 3 Schritte abgearbeitet. So einfach ist das. Ich persönlich orientiere mich da gern an folgender Aussage.

Jeffrey Way

The wonderful thing about TDD is that it turns your coding process into a game. Not sure what to do next? Run the tests. […] TDD is about tiny tiny steps.

Denn genauso kann man das gesamte Szenario betrachten. Doch nach dieser kurzen theoretischen Einführung möchte ich kurzerhand dieses Vorgehen an einem sehr einfachen Beispiel praktisch erklären, doch vorher sollte PHPUnit als Testframework in das Projekt aufgenommen werden.

 

PHPUnit einbinden – Composer for the win

Mit Composer, einem Abhängigkeit-Verwaltungswerkzeug, ist das ganze kein aufwendiger Prozess mehr, vorausgesetzt Composer ist schon installiert. – Wie das geht, steht in der offiziellen Anleitung.

Als Erstes wird also ein leerer Projektordner mit folgender Struktur erstellt.

tddexample
	src
	tests
	composer.json
	.gitignore

In der composer.json-Datei fordern wir das benötigte PHPUnit-Framework für die Entwicklungsumgebung(dev) an.  Im Produktiv-Einsatz braucht PHPUnit nicht abgerufen werden, daher wird die Abhängigkeit im Bereich require-dev eingetragen. Die folgende composer.json-Datei kann verwendet werden.

{
    "name": "codefever/tddexample",
    "description": "",
    "authors": [
        {
            "name": "Eric Kubenka",
            "email": "info@code-fever.de"
        }
    ],
    "require": {
        "php": ">=5.3.0"
    },
    "require-dev": {
    	"phpunit/phpunit": "3.7.*"
    },
    "autoload": {
        "classmap": [
            "src",
            "tests"
        ]
    },
    "minimum-stability": "dev"
}

Nach dem Ausführen von composer install --dev steht PHPUnit nun zur Verfügung und es kann losgehen. Folgende phpunit.xml-Datei sollte noch in das root-Verzeichnis des Projekts eingebunden werden.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap=""
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false"
>
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
</phpunit>

 

First: Test – Einen fehlschlagenden Test schreiben

Wie eben gelernt, wird nicht einfach wild angefangen nun irgendwelchen Quellcode zu schreiben, da das Vorgehen nach TDD einen dazu zwingt vorher über seinen zukünftigen Sourcecode nachzudenken.

Ziel des folgenden Beispiels ist es mittels der Methode create() und der Parameter $url und $text in der Klasse Anchor einen HTML-Link zu generieren. Aus diesen Bedingungen und Überlegungen, welche Aufgaben die zu testende Methode übernehmen soll ,lässt sich folgender Testfall ableiten.

<?php

/**
 * Test the anchor class
 * @package default
 */
class AnchorTest extends PHPUnit_Framework_TestCase
{
	/**
	 * test that the create method works as epected
	 * @return type
	 */
	public function testCreateAnchor()
	{
		// arrange
		$anchor = new Anchor;

		$expected = "<a href='http://code-fever.de'>Code Fever</a>";
		
		// act
		$actual = $anchor->create('http://code-fever.de', 'Code Fever');

		// assert
		$this->assertSame($expected, $actual);
	}
}

 

Second: Code! – Den Test erfolgreich werden lassen

Doch wie geht es nun weiter? Führt den Test einfach mittels folgender Kommandozeile ausgehend vom root-Verzeichnis aus. Zuvor sollte ein composer dump-autoload –o ausgeführt werden.

// ensure that alle new classes are included
composer dump-autoload -o

// run php unit from root
// php unit will use default-configuration file phpunit.xml of the root dir
vendor/bin/phpunit

Und nun wird deutlich was es heißt, wenn der Test vor der eigentlichen Methode entwickelt worden ist. Es hagelt Exceptions. Aber dadurch erfährt man von PHPUnit, welcher Schritt als nächstes folgt.

Fatal Error: Class Anchor not found […]

Es existiert keine Klasse namens Anchor? Dann wird eben eine erstellt.

<?php

/**
 * Anchor Management Class to handle the generation of anchors
 * @package default
 */
class Anchor
{

}

Nach einer Wiederholung der zwei oben stehenden Befehle wird mitgeteilt, dass die Methode create() nicht existiert. Dann wird diese als Nächstes angelegt. Wie gesagt, man arbeitet sich beim TDD in kleinen Schritten zum Produktiv-Code.

<?php

/**
 * Anchor Management Class to handle the generation of anchors
 * @package default
 */
class Anchor
{
	/**
	 * Create a Anchor with url and displayed text
	 * @param type $url 
	 * @param type $link 
	 * @return type
	 */
	public function create($url, $link)
	{

	}
}

Nach Ausführung der Tests erhält man nun das erste wirkliche Testergebnis

Failed asserting that null is identical to [...]

Das Problem ist einfach: Aktuell gibt die Methode noch null (kein Rückgabewert) zurück. Der nächste Schritt ist nun den Test erfolgreich werden zulassen. Um weiterhin nach dem Motto „Kleine Schritte“ zu arbeiten, wird die Methode create() wie folgend verändert und der Test ausgeführt.

public function create($url, $link)
{
	return "<a href='http://code-fever.de'>Code Fever</a>";
}

Großartig! Soeben wurde die erste Methode der Klasse Anchor erfolgreich mittels testgetriebener Entwicklung erstellt. Damit sind die ersten beiden Schritte des 3-Stufen-Plans beim TDD abgeschlossen. Nun geht es über zum dritten Teil.

 

Third: Refactor – Den Quellcode an Gegebenheiten anpassen

Wie jedem Leser bereits sicherlich klar geworden ist, gibt die erstellte Methode create() bisher nur einen statischen Wert zurück – nämlich genau den Wert, welcher auch erwartet wird. Eine Veränderung der Eingabeparameter bewirkt bisher nichts. Genau diesem Verhalten wird jetzt entgegen gewirkt. Die Methode create() wird folgend umgeschrieben.

<?php

/**
 * Anchor Management Class to handle the generation of anchors
 * @package default
 */
class Anchor
{
	/**
	 * Create a Anchor with url and displayed text
	 * @param type $url 
	 * @param type $link 
	 * @return type
	 */
	public function create($url, $link)
	{
		return "<a href='{$url}'>{$link}</a>";
	}
}

Und siehe da, wie erwartet bleibt der geschriebene Test erfolgreich.

 

Fazit

Ich hoffe ich konnte Euch Lesern das Verfahren TDD auf eine verständliche Art und Weise erklären. Natürlich wurde mit dem Artikel gerade mal an der Oberfläche gekratzt und noch ist kein Wort über Mocking und Co. gefallen. Jedoch hoffe ich, dass mit diesem Artikel so mancher einen einfacheren Einstieg in diese ganze Thematik findet.

Quellcode: Der gesamte Quellcode steht auf GitHub bereit.

Zurück