Observer Pattern mit PHPUnit testen
Geschrieben von Dejan Spasic • Tuesday, 5. May 2009 • Kategorie: PHP • Kommentar (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!

