Observer Pattern mit PHPUnit testen

Geschrieben von Dejan Spasic • Tuesday, 5. May 2009 • Kategorie: PHPKommentar (1)
Der Observer (Beobachter, Listener) ist ein Entwurfsmuster aus dem Bereich der Softwareentwicklung und gehört zu der Kategorie der Verhaltensmuster (Behavioural Patterns). Es dient zur Weitergabe von Änderungen an einem Objekt an von diesem Objekt abhängige Strukturen. Das Muster ist eines der sogenannten GoF-Muster (Gang of Four).
Quelle: Wikipedia

Die Klassen

Da SPL schon für das Entwurfmuster Schnittstellen (SplSubject, SplObserver) bereit stellt, werden wir auch diese für dieses Beispiel einsetzen.

Klasse Observer

<?php
/**
 * A Observer
 * 
 * @category   UnitTests
 * @package    Ddown
 * @subpackage DesignPatterns
 * @version    Number one
 */
class Observer implements SplObserver
{
	/**
	 * @param SplSubject $subject
	 * @return string
	 */
	public function update(SplSubject $subject)
	{
		return $subject->getValue();
	} 
} 

Klasse Subject

<?php
/**
 * The subject
 *
 * @category   UnitTests
 * @package    Ddown
 * @subpackage DesignPatterns
 * @version    Number one
 */
class Subject implements SplSubject
{
	/**
	 * @var SplObserver
	 */  
	private $observers = array();
 
	/**
	 * @var string
	 */	
	private $value = null;
 
	/**
	 * Attach a observer
	 * @param SplObserver $observer
	 * @return void
	 */
	public function attach(SplObserver $observer)
	{
		//wir fuegen hier den observer hinzu
		// um zu verhindern das der gleiche $observer hinzugefuegt
		// werden kann, verwenden wir einen eindeutigen hash des 
		// uebergebenden observers
		$this->observers[spl_object_hash($observer)] = $observer;
	}
 
	/**
	 * Detach a observer
	 * @param SplObserver $observer
	 * @return void
	 */
	public function detach(SplObserver $observer)
	{
		unset($this->observers[spl_object_hash($observer)]);
	}
 
	/**
	 * Notify all attachted observer
	 * @param SplObserver $observer
	 * @return void
	 */
	public function notify()
	{
		foreach ($this->observers as $observer)
		{
			$observer->update($this);
		}
	} 
 
	/**
	 * Setter for sValue attribute
	 *
	 * Additionaly it notifies the attachted observers
	 * @param string $value
	 * @return void
	 */
	public function setValue($value)
	{
		$this->value = $value;
		$this->notify();
	}
 
	/**
	 * Getter for sValue attribute
	 * @param SplObserver $observer
	 * @return void
	 */
	public function getValue()
	{
		return $this->value ;
	}
}

Die Tests

Kommen wir nun zu den Tests.

Den Observer testen

Als erstes testen wir den Observer, um fest zu stellen, ob es auch den Wert des Subject zurückgibt. Der positive Nebeneffekt dieses Tests ist, das wir auch zugleich den Setter und Getter des Subjects testen.

<?php
 
// Das PHPUnit Framework laden
require_once 'PHPUnit/Framework.php';
 
// Die Pattern Klasse fuer die Tests laden
require_once dirname(_FILE_) . '/Observer.php';
require_once dirname(_FILE_) . '/Subject.php';
 
/**
 * Testcase for observer
 * 
 * @category   UnitTests
 * @package    Ddown
 * @subpackage DesignPatterns
 * @version    Number one
 */
class ObserverTest extends PHPUnit_Framework_TestCase
{
	/**
	 * Test update method 
	 * @return void
	 * @covers Observer::update
	 * @covers Subject::setValue
	 * @covers Subject::getValue
	 */
	public function testUpdate()
	{
		$subject  = new Subject();
		$observer = new Observer();
 
		$subject->setValue('Observer Pattern');
 
		self::assertEquals($observer->update($subject), $subject->getValue());
	} 
}

Die Klasse Subject testen

Jetzt kommt die Subject-Klasse dran. Als erstes werden wir der Vollständigkeitshalber ebenfalls den Setter und Getter testen.

<?php
 
// Das PHPUnit Framework laden
require_once 'PHPUnit/Framework.php';
 
// Die Pattern Klasse fuer die Tests laden
require_once dirname(_FILE_) . '/Observer.php';
require_once dirname(_FILE_) . '/Subject.php';
 
/**
 * Testcase for observer
 * 
 * @category   UnitTests
 * @package    Ddown
 * @subpackage DesignPatterns
 * @version    Number one
 */
class SubjectTest extends PHPUnit_Framework_TestCase
{
	/**
	 * @var Subject
	 */
	protected $subject = null;
 
	/**
         * setup for each test
         * @return void
	 */
	protected function setUp()
	{
		$this->subject = new Subject();
	}
 
	/**
	 * test setter and getter for value attribute
	 * @return void
	 */
	public function testSetGetValue()
	{
		$value = 'DDown';
		$this->subject->setValue($value);
		self::assertEquals($this->subject->getValue(), $value);
	}
} 

Gut weiter im Text. Jetzt kommen wir zu unserer eigentliche Aufgabe. Um das Pattern zu Testen werde ich hier nicht die Obeserver-Klasse selbst verwenden, sondern ein sogenanntes Mock-Objekt. Der Grund ist einfach, mit den Mock-Objekt kann ich das Verhalten genauer testen, als mit unsere Observer-Klasse. Z.B. kann ich überprüfen wie oft ein Oberser bzw. die notfiy Methode aufgerufen wurde. Diese Tests könnten wir mit unserer jetzigen Observer-Klasse, ohne sie für die Tests auf zu bohren, nicht ausführen.

        /**
	 * Test the observer pattern
	 * @return void
	 */
	public function testNotify()
	{
		// Da wir gewaehrleisten muessen, dass das Mock-Objekt die SplObserver 
		// Schnittstelle implementiert, geben wir diesen als Klassennamen an.
		$observer = $this->getMock('SplObserver', array('update'));
 
		// Nun kommt der interessante Teil. Wir moechten folgendes testen:
		// * Die Methode update wird nur EINMAL aufgerufen
		// * Das uebergeben Argument ist vom TYP SplSubject
		$observer->expects($this->once())
				  ->method('update')
				  ->with($this->isInstanceOf('SplSubject'));
 
		// Na dann mal los.
		$this->subject->attach($observer);
		$this->subject->setValue('notify now!');
	} 

Fertig. Wenn ihr mir oder dem Mock Objekt nicht glaubt, probiert es selbst aus, gibt an das die Methode update nie aufgerufen werden darf.

...
$observer->expects($this->never());
...

Das Resultat ist folgender:

$ phpunit SubjectTest
PHPUnit 3.3.14 by Sebastian Bergmann.

.F

Time: 0 seconds

There was 1 failure:

1) testNotify(SubjectTest)
SplObserver::update(Subject(...)) was not expected to be called.
/usr/share/php/PHPUnit/Framework/MockObject/Mock.php(228) : eval()'d code:28
...unittests/pattern/observer/Subject.php:59
...unittests/pattern/observer/Subject.php:74
...unittests/pattern/observer/SubjectTest.php:67

FAILURES!
Tests: 2, Assertions: 1, Failures: 1.

So jetzt testen wir das abmelden eines Observers. Auch hier verwenden wir ein Mock-Objekt.

       /**
	 * Test the observer pattern
	 * @return void
	 * @covers Subject::detach
	 * @covers Subject::notify
	 */
	public function testDetach()
	{
		// Da wir gewaehrleisten muessen, dass das Mock-Objekt die SplObserver 
		// Schnittstelle implementiert, geben wir diesen als Klassennamen an.
		$observer = $this->getMock('SplObserver', array('update'));
 
		// * Die Methode update darf kein mal aufgerufen werden
		$observer->expects($this->never())
				  ->method('update');
 
		// Na dann mal los.
		$this->subject->attach($observer);
		$this->subject->detach($observer);
		$this->subject->setValue('notify now!');
	} // function

Der letzte Test denn wir noch benötigen, ist das Gewährleisten eines nicht redunanten Observers im Stack.

       /**
	 * Warrants that only one instance of each observer can be attached 
	 * @return void
	 * @covers Subject::attach
	 * @covers Subject::notify
	 */
	public function testNoRedundantObservers()
	{
		// Da wir gewaehrleisten muessen, dass das Mock-Objekt die SplObserver 
		// Schnittstelle implementiert, geben wir diesen als Klassennamen an.
		$oObserver = $this->getMock('SplObserver', array('update'));
 
		// * Die Methode update wird nur EINMAL aufgerufen
		// * Das uebergeben Argument ist vom TYP SplSubject
		$observer->expects($this->once())
				  ->method('update')
				  ->with($this->isInstanceOf('SplSubject'));
 
		// Na dann mal los.
		$this->subject->attach($observer);
		$this->subject->attach($observer);
		$this->subject->setValue('notify now!');
	}

Refactoring

Da wir gerade von Redundanz sprechen. Es ist euch bestimmt aufgefallen, dass wir in den letzten drei Tests fast immer das selbe Mock-Objekt erzeugen. Der eigentlich Unterschied besteht darin wie oft die Methode update aufgerufen werden soll. Und das stinkt und schreit förmlich nach Refactoring. Denn auch Tests wollen lesbar und pflegbar sein.

Ich verwende Methode extrahieren für das Erzeugen eines Mock-Objekts.

       /**
	 * Create a mock object which implements SplObserver interface
	 * @return SplObserver
	 */
	protected function createMockObserver()
	{
		// Da wir gewaehrleisten muessen, dass das Mock-Objekt die SplObserver 
		// Schnittstelle implementiert, geben wir diesen als Klassennamen an.
		return $this->getMock('SplObserver', array('update'));
	}

Und verwende diese Methode nun in alle drei Tests. Teste meine Tests um zu schauen ob alles noch läuft! Jup läuft. Okay weiter.

Ich verwende nochmals Methode extrahieren für die Definition der Methode update. Die neue Methode erwartet zwei Argumente. Das Erste ist das Mock-Objekt und das Zweite gibt an wie oft die Methode update aufgerufen werden soll.

       /**
	 * @param object $mockObserver
	 * @param int $invokedCount
	 * @return void
	 */
	protected function implementExpectationsForMockObserver($mockObserver, $invokedCount)
	{
		$method = $mockObserver->expects($this->exactly($invokedCount))->method('update');
 
		if (0 < $invokedCount)
		{
			$method->with($this->isInstanceOf('SplSubject'));
		}
	}

Nun ersetze ich den Code in den Tests und teste noch mal alles. Funtzt!

Tags für diesen Artikel: , , ,

BOM (Byte Order Mark) finden

Geschrieben von Dejan Spasic • Tuesday, 5. May 2009 • Kategorie: QuerbeetKommentare (0)

Es kann schonmal vorkommen das sich, aus welchem Grund auch immer, in irgendeiner Datei ein sogenanntes BOM eingenisstet hat. Wenn das Projekt nun einige tausende Dateien mit sich bringt, sucht man sprichwörtlich die Nadel im Heuhaufen. Abhilfe verschafft uns hier das Kommando fgrep.

fgrep -rl `echo -ne '\xef\xbb\xbf'` .

Man könnte nun die gefundene Datei mit vim öffnen und mittels Kommando :%!xxd die Datei in hex dump konvertieren, womit der BOM entfernt werden kann.

Tags für diesen Artikel: , , ,

Filtering & Escaping Cheat Sheet

Geschrieben von Dejan Spasic • Monday, 4. May 2009 • Kategorie: PHPKommentare (0)

Auf der Seite Filtering & Escaping Cheat Sheet findet Ihr einen nützlichen Cheat Sheet über die Filter- und Maskierungstrategien die man in jeden Fall einsetzen sollte.

Tags für diesen Artikel: ,

PHP Bug 48139

Geschrieben von Dejan Spasic • Monday, 4. May 2009 • Kategorie: PHP , QuerbeetKommentare (0)

Der Bugreport 48139. Ich weiss nicht was ihr davon haltet, aber ich habe mich köstlich amüsiert.

Tags für diesen Artikel: ,