Unittest erstellen

PHPUnit installieren

Sehr einfach ist die Installation unter Ubuntu. Dort kann phpunit einfach über aptitude installiert werden:

aptitude install phpunit

Eine weitere Möglichkeit ist die Installation von phpunit über pear:

pear channel-dicover pear.phpunit.de
pear install phpunit/PHPUnit

Nachdem eine Methode zur Installtion genutzt wurde, sollte man phpunit auf der Shell ausführen können.

phpunit --version

Testfälle mit PHPUnit erstellen

Da Testfälle nicht mit dem Produkt ausgeliefert werden sollen macht es Sinn diese in einem eigenen Verzeichnis zu speichern. Ich verwenden bei diesem Projekt folgende Struktur:

  1. Project/Classes zum speichern der Klassen

  2. Project/Tests zum speichern der Testfälle


Wir beginnen zunächst mit einem einfachen Beispiel:

Wir haben eine „PersonCollection“ mit einer Sammlung von Personen. Für diese Collection möchten wir eine Methode implementieren, die uns alle Personen liefert, deren Nachname mit einem einem bestimmten Text beginnt.

Anmerkung: Diese Implementierung ist nur gedacht für kleine Collections die man alle im Speicher halten kann, bei größeren Collections würde man hier natürlich anders vorgehen ;)

Wir beginnen mit einem ersten Wurf des Testfalles und der Klassenstrutkur.

Zunächst der Testfall (Tests/PersonCollectionTest.php):

<?php
require_once '../Classes/Person.php';
require_once '../Classes/PersonCollection.php';

class PersonCollectionTest extends PHPUnit_Framework_TestCase {
	/**
	* @var PersonCollection
	*/		
	protected $collection;

	/**
	* @return void
	*/
	public function setUp() {
		$this->collection = new PersonCollection();
	}

	/**
	* @test
	*/
	public function testGetLastNameStartingWith() {
		$karl   = new Person('Karl','Meyer');
		$willi	   = new Person('Willi','Schneider');
		$peter = new Person('Peter','Müller');
		$frida  = new Person('Fireda','Salomon');
		$fred   = new Person('Fred','Schmidt');

		$this->collection->add($karl)->add($willi)->add($peter);
		$this->collection->add($frida)->add($fred);

		$schItems = $this->collection->getLastNameStartingWith('Sch');
		$schCount = $schItems->count();

		$errorMsg = 'Unexpected itemcount starting with Sch';
		$this->assertEquals(2,$schCount,$errorMsg);

		$sItems   = $this->collection->getLastNameStartingWith('S');
		$sCount   = $sItems->count();
		$errorMsg = 'Unexpected itemcount starting with s';

		$this->assertEquals(3,$sCount,$errorMsg);
	}
}
?>

Ein Testfall hat folgenden Aufbau:

  1. Die Testklasse muss erben von „PHPUnit_Framework_TestCase“ dies ist die Basisklasse im PHPUnit Framework.

  2. In der Testklasse können Testfälle definiert werden, indem man der Methode the Annotation @test gibt, oder indem der Methodenname mit „test“ beginnt.

  3. Es gibt „assert*“ Methoden, mit denen man eigene Erwartungen formulieren kann. Werden alle Erwartungen erfüllt ist der Test grün. Wird eine Erwartung nicht erfüllt, so ist der Test rot. In unserem Fall erwarten wir das zwei Personen gefunden werden, die mit „Sch“ beginnen und drei Personen die mit „S“ beginnen. Sollte dies nicht der Fall sein, schlägt der Test fehl.

  4. Die Methode „setUp“ wir vor jedem Testfall ausgeführt. Also bevor jeder einzele Testmethode ausgeführt wird. Diese Methode ist dazu gedacht die Rahmenbedingungen für den Testfall herzustellen. In diesem Fall wird nur im Attribut „collection“ die Instanz der zu testenden Collection erstellt.

  5. Die Methode „tearDown“ ist das Gegenstück zu „setUp“ sie wird nach jedem Testfall ausgeführt. Im oberen Fall wurde sie nicht benötigt.


Damit der Test zunächst einmal genutzt werden kann. Müssen die verwendeten zu testenden Klassen angelegt werden.

Die Klasse Person(Classes/Person.php), sieht wie folgt aus :

<?php
class Person {
	/**
	* @var string
	*/
	protected $firstName;

	/**
	* @var string
	*/
	protected $lastName;

	/**
	* @param string $theFirstName
	* @param string $theLastName
	*/
	public function __construct($theFirstName, $theLastName) {
		$this->firstName = $theFirstName;
		$this->lastName = $theLastName;
	}

	/**
	* @return string
	*/
	public function getLastName() {
		return $this->lastName;
	}
}
?>

Die Klasse PersonCollection(Classes/PersonCollection) hat folgenden Aufbau:


<?php
class PersonCollection {
	/**
	* @var SplObjectStorage
	*/
	protected $persons;

	public function __construct() {
		$this->persons = new SplObjectStorage();
	}

	/**
	* @param Person $person
	* @return PersonCollection
	*/
	public function add(Person $person) {
		$this->persons->attach($person);
		return $this;
	}

	/**
	* @return int
	*/
	public function count() {
		return $this->persons->count();
	}

	/**
	* This method retrieves a collection of persons
	* where the lastname is starting with the passed
	* substring.
	*
	* @return PersonCollection
	*/
	public function getLastNameStartingWith($substring) {
		return $result;
	}
}
?>

Zunächst wurde nur die Strutkur angelegt und die Methode getLastnameStartingWith() hat noch keine Funktionalität. Diese Funktionalität soll nun Schritt für Schritt entwickelt werden.

Zunächst führen wir den Testfall aus und stellen fest, dass er fehlschlägt:



cd Tests/
phpunit PersonCollectionTest.php

PHPUnit 3.4.12 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 8.00Mb

There was 1 failure:
1) PersonCollectionTest::testGetLastNameStartingWith
Unexpected itemcount starting with Sch

Failed asserting that <null> matches expected <integer:2>.

/home/timo/Desktop/phpunit/Tests/PersonCollectionTest.php:29


Der Ausgabe kann entnommen werden, dass die Methode keine Collection zurückgibt. Der nächste Schritt ist die Implementierung der Methode.

Der nächste Versuch sieht so aus:


...

public function getLastNameStartingWith($substring) {	
	$result = new PersonCollection();
	$length = mb_strlen($substring);

	foreach($this->persons as $person) {
		/** @var $person Person*/
		$lastName = $person->getLastName();
		$sameLengthPrefix = mb_substr($lastName,0,$length);
		if($sameLengthPrefix == $substring) {
			$result->add($person);
		}
	}
	return $result;
}

...

Zugegebnermaßen enstand dies nicht beim erten Testlauf, denn Tippfehler etc. passieren immer wieder. Wenn wir die Funktionalität aber so umsetzen. So wird unser Testfall grün:

PHPUnit 3.4.12 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 8.00Mb

OK (1 test, 2 assertions)


Refactoring - Getesteten Code anpassen

Nun ist einige Zeit vergangen und wir haben eine Idee wie die Funktion verbessert werden kann.

Mit der PHP Funktion iterator_apply und einer Closure erwarten wir eine schnellere Ausführung. Dank unseres Testfalles können wir sicherstellen, dass der Code noch unseren Erwartungen entspricht:

Folgendermaßen passen wir die Methode an:

...

public function getLastNameStartingWith($substring) {
	$result = new PersonCollection();
	$length = mb_strlen($substring);

	iterator_apply(
		$this->persons, 
		function($iterator) use (&$result, &$length, &$substring) {
			/** @var $person Person*/
			$person = $iterator->current();
			$lastName = $person->getLastName();
			$sameLengthPrefix = mb_substr($lastName,0,$length);

			if($sameLengthPrefix == $substring) {
				$result->add($person);
			}

			return true;

		},
		array($this->persons)
	);
	return $result;
}

...

Der PHPUnit Testcase stellt sicher, dass wir für unsere automatisierten Testfälle noch korrekte Ergebnisse bekommen. Ob die Änderun sich tatsächlich positiv auf die Performance auswirkt, muss anders ermittelt werden und wird später im Abschnitt „Performancetunning“ noch genauer beschrieben.


Tipps:

  1. Erstelle für deine Testfälle immer einer eigene Basistestklasse, die von PHPUnit_Testcase erbt. Dort kannst du zentrale Methoden implementieren, die du in allen Testfällen brauchst und kannst dort Änderungen durchführen wenn sich etwas in PHPUnit ändert (das ist selten notwendig.)


Ich hoffe dieser Artikel konnte einen Einstieg in die testgetriebene Entwicklung bieten. Das Ziel eines Testfalles ist es, nur die Funktionalität zu testen, für die der Testfall geschrieben wurde.

Im vorherigen Beispiel wurde jedoch mehr getestet. Neben der Funktionalität der „PersonCollection“ wurde indirekt auch die Funktionalität der „Person“ Klasse getestet. Bei einem so einfachen Beispiel ist das noch ok. Bei größeren Klassen jedoch nicht. Zu diesem Zweck gibt es „Mock“ Objekte. Ein „Mock“ Objekt dient dazu ein andere Objekt zu simulieren.

Im oberen Beispiel würde dies bedeuten. Das wir der Collection keine Personen Objekte übergeben, sondern Mock Objekte, die sich wie ein Personen Objekt verhalten.

Für den Beispieltestfall reicht es auch dass das Mock Objekt die Methode „getFirstName“ simuliert.



Mehr zum Thema Mock Objekte folgt im nächsten Abschnitt.


Navigation