Archiv für die Kategorie ‘Test-Driven Development’
Geschützte (protected) Methoden mittels PHPUnit testen
Gelegentlich ist das Testen von als “protected” deklarierten Methoden mittels PHPUnit unumgänglich. Folgende Testcase-Methode macht’s möglich, setzt allerdings PHP 5.3 voraus:
/**
* @param string $name
* @param string $classname
* @param array $params
*/
protected static function callProtectedMethod($name, $classname, array $params) {
$class = new ReflectionClass($classname);
$method = $class->getMethod($name);
$method->setAccessible(true);
$obj = new $classname();
return $method->invokeArgs($obj, $params);
}
PHPUnit: “at”-Matcher liefert “Mocked method does not exit” zurück
Bei der Arbeit mit Mock-Objekten hatten wir kürzlich mit dem Problem zu kämpfen, dass ein Test bei Verwendung des “at”-Matchers ein “Mocked method does not exit” zurückgab, obwohl die Mock-Methode zweifelsfrei existierte.
Nach ein wenig googeln fand sich schließlich der folgende, sehr aufschlussreiche Artikel:
PHPUnit “Mocked method does not exist.” when using $mock->expects($this->at(…))
Test-Driven Development unter Typo3: Extbase-Controller testen
Um Controller-Klassen unter dem Typo3-Extbase-Framework testen zu können, gilt es einige Unwegsamkeiten zu bewältigen:
- Bestimmte zu testende Member-Variablen der Controller-Klasse wie beispielsweise die ‘view’-Variable sind als “geschützt” deklariert und sind zunächst weder über Setter/Getter noch per Dependency-Injection erreichbar
- Von Haus unterstützt PHPUnit weder das “Stubben” noch “Mocken” von Methoden, die als privat, geschützt oder statisch definiert sind
Glücklicherweise erweitert die PHPUnit-Extension für Typo3 das PHPUnit-Framework um die nötigen Methoden, um “Accessible Mocks” zu erzeugen. Die entsprechende Methode getAccessibleMock() ermöglicht so den Zugriff auf geschützte Methoden und Eigenschaften des Mock-Objekts.
Ein Setup für eine Extbase-Controller-Testklasse könnte wie folgt aufgebaut sein:
/**
* @var Tx_Cgfaq_Controller_FaqController
*/
protected $fixture;
public function setUp() {
$mockFaqRepository = $this->getMock(
'Tx_Cgfaq_Domain_Repository_FaqRepository', array(), array(), '', FALSE
);
$this->fixture = $this->getAccessibleMock(
'Tx_Cgfaq_Controller_FaqController', array('dummy'), array(), '', FALSE
);
$this->fixture->injectFaqRepository($mockFaqRepository);
$this->fixture->_set('view', $this->getMock('Tx_Fluid_View_TemplateView', array(), array(), '', FALSE));
}
Hierbei erzeugen wir zunächst die benötigten Mock-Objekte; eines für die Repository- und eines für die Controller-Klasse. Das Mock-Objekt für die Controller-Klasse erzeugen wir lediglich, um die geschützten Methoden und Eigenschaften per getAccessibleMock() durch öffentliche zu ersetzen. Damit PHPUnit nicht alle Methoden der Controller-Klasse durch Stubs ersetzt (was die Vorteile mit einem Schlage wieder wett machen würde), muss mit dem Aufruf der getAccessibleMock()-Methode zwingen eine Dummy-Methode übergeben werden (hier: array(‘dummy’)).
Die getAccessibleMock()-Methode erweitert die angegebene Klasse um drei Funktionen, mit denen der Zugriff auf die vormals geschützten Eigenschaften und Methoden möglich wird:
- _set($propertyName, $value)
- _setRef($propertyName, &$value)
- _get($propertyName)
Ist die setUp()-Methode geschrieben, sind die nachfolgenden Testfälle schnell getippt:
/**
* @test
*/
public function listActionFindsDemandedFaqs() {
$mockQueryResult = $this->getMock('Tx_Extbase_Persistence_QueryResult', array(), array(), '', FALSE);
$this->fixture->_get('faqRepository')->expects($this->once())
->method('fetchByFeUserAndGroup')
->will($this->returnValue($mockQueryResult));
$this->fixture->_get('view')->expects($this->once())
->method('assign')
->with($this->equalTo('faqs'),
$this->equalTo($mockQueryResult)
);
$this->fixture->listAction();
}
ZendServer CE (Windows-Version): PEAR und PHPUnit installieren
Leider liefert Zend die ZendServer Community Edition ohne PEAR aus, sodass sich auch PHPUnit nicht ohne Weiteres installieren respektive einsetzen lässt. Der kürzeste uns bekannte Weg führt daher über die Kommandozeile:
PEAR installieren:
- Als erstes empfiehlt es sich, eine aktuelle Version der go-phear.phar-Datei herunterzuladen und in das PEAR-Verzeichnis (siehe Schritt 3) zu kopieren.
- Dann startet man die Kommandozeile mit Admin-Rechten
- Anschließend gibt man den folgenden Befehl ein: C:\Program Files\Zend\ZendServer\bin\PEAR>php -d phar.require_hash=0 go-pear.phar
- Im Laufe der Installation ist es noch wichtig, den Pfad zur CLI php.exe-Datei einzugeben, die üblicherweise unter C:\Program Files\Zend\ZendServer\bin liegt
- Auf keinen Fall sollte man vergessen, nach der Installation PHP neu zu starten, da die PEAR-Installation auf Wunsch die Include-Pfade in der php.ini-Datei anpasst.
- Hat alles geklappt, empfiehlt es sich, PEAR zu aktualisieren: pear upgrade pear
PHPUnit installieren:
Ist PEAR einsatzbereit und auf dem aktuellen Stand, kann PHPUnit samt Abhängigkeiten wie im PHPUnit-Manual beschrieben mit den folgenden Befehlen installiert werden:
- pear channel-discover pear.phpunit.de
- pear channel-discover components.ez.no
- pear channel-discover pear.symfony-project.com
- pear install phpunit/PHPUnit
Anschließend empfiehlt es sich, unter Windows die path-Umgebungsvariable um den Programmpfad zur phpunit.bat zu erweitern, um sich lästige Tipparbeit respektive Verzeichniswechsel zu ersparen.
Ob PHPUnit korrekt installiert ist, lässt sich abschließend mit dem Befehl “phpunit” auf der Kommandozeile überprüfen. Dies offenbart unter anderem auch, dass die derzeit per PEAR verteilte PHPUnit-Version gnadenlos veraltet ist…-
Test-Driven Development unter Typo3: Extbase-Repositories testen
Mit der Einführung des “Testing Framework” in die Typo3-PHPUnit-Extension hat sich das Testen von Repositories ausgesprochen vereinfacht. Neben dem testweisen Schreiben von Datensätzen in die Datenbank bietet die Tx_Phpunit_Framework-Klasse eine Vielzahl weiterer nützlicher Funktionen wie beipspielsweise das testweise Anlegen von Front- und Backend-Usern oder das Ein- und Ausloggen von Nutzern.
Einen Überblick über die Möglichkeiten des Testing Frameworks findet sich in der Doku der Typo3-PHPunit-Extension.
Wie bei jeder PHPUnit-Testklasse schreiben wir zunächst die setUp()-Methode in der wir neben dem Fixture auch eine Instanz des Testing Frameworks instantiieren sowie die tearDown()-Methode. Da beim Testen der Repository-Klassen zwangsläufig externe Ressourcen einbezogen werden (Datenbank), ist die tearDown-Methode von besonderer Wichtigkeit, um nach jedem Test die externen Ressourcen wieder in den Ausgangszustand zurückzuversetzen:
/**
* @var Tx_Phpunit_Framework
*/
protected $testingFramework;
/**
* @var Tx_Cgfaq_Domain_Repository_FaqRepository
*/
protected $fixture;
public function setUp() {
$this->testingFramework = new Tx_Phpunit_Framework('tx_cgfaq');
$this->fixture = $this->objectManager->get('Tx_Cgfaq_Domain_Repository_FaqRepository');
}
public function tearDown() {
$this->testingFramework->cleanUp();
unset($this->testingFramework, $this->fixture);
}
In den einzelnen Tests generieren wir zunächst einige Dummy-Datenbankeinträge, führen die entsprechenden Repository-Methoden aus und prüfen schlussendlich das Rückgabeergebnis:
/**
* @test
* @return void
*/
public function fetchByFeUserAndGroup_testing_SingleUserConstraint() {
$pid = 0;
$mockCustomerRelatedService = $this->getMock('Tx_Cgfaq_Service_CustomerRelatedService');
$this->fixture->injectCustomerRelatedService($mockCustomerRelatedService);
// create some dummy records
$this->testingFramework->createRecord('tx_cgfaq_domain_model_faq', array(
'pid' => $pid,
'type' => Tx_Cgfaq_Controller_FaqController::SIMPLE_FAQ,
'question' => 'OKAY',
'answer' => 'OKAY',
'front_user'=> 99
));
$this->testingFramework->createRecord('tx_cgfaq_domain_model_faq', array(
'pid' => $pid,
'type' => Tx_Cgfaq_Controller_FaqController::SIMPLE_FAQ,
'question' => 'AHA',
'answer' => 'AHA',
'front_user'=> 99
));
$this->testingFramework->createRecord('tx_cgfaq_domain_model_faq', array(
'pid' => $pid,
'type' => Tx_Cgfaq_Controller_FaqController::SIMPLE_FAQ,
'question' => 'ERROR',
'answer' => 'ERROR',
'front_user'=> 88
));
$mockCustomerRelatedService->expects($this->any())
->method('isGroupMember')
->will($this->returnValue(FALSE));
$mockCustomerRelatedService->expects($this->any())
->method('getCalculatedFeUser')
->will($this->returnValue(99));
$this->assertEquals(2, (int)$this->fixture->fetchByFeUserAndGroup()->count());
}
Tipp: Beim Testen von Repository-Methoden, die den Inhalt der Datenbank verändern (z.B. per remove() oder update()), müssen vor der Fomulierung der Annahmen ($this->assert…..) die durch die Repository-Methoden eingeleiteten Änderungen persistiert werden. Dazu genügen die folgenden beiden Zeilen:
$persistanceManager = t3lib_div::makeInstance('Tx_Extbase_Persistence_Manager');
$persistanceManager->persistAll();
Test-Driven Development unter Typo3: Extbase-Models testen
In diesem und den folgenden Beiträgen möchten wir unsere Erfahrungen für die testgetriebene Entwicklung (TDD) unter Typo3 Extbase/Fluid beschreiben sowie einige “Best Practice” vermitteln.
Für das Testen von Models hat sich folgende Herangehensweise bewährt:
Zunächst erfolgt das Aufsetzen des Testobjekts (“fixture”):
/**
* @var Tx_Cgfaq_Domain_Model_Faq
*/
protected $fixture;
public function setUp() {
$this->fixture = new Tx_Cgfaq_Domain_Model_Faq();
}
Solange keine externen Ressourcen für die Erstellung des Testobjekts benötigt werden, kann auf eine explizite tearDown()-Methode verzichtet werden.
Setter und Getter für einfache Objekteingenschaften testen wir wie folgt. Dabei greifen wir wann immer es sinnvoll ist auf “assertSame” statt auf “asserEquals” zurück. So können wir sicher sein, dass sowohl der Typ der Eigenschaft als auch deren Wert auf Gleichheit geprüft werden. Zusätzlich prüfen wir, dass jede Eigenschaft den von uns erwarteten Ausgangswert besitzt:
/**
* Test if getTitle() returns inital value for title
*
* @test
* @return void
*/
public function getTitleReturnsInitialValueForTitle() {
$this->assertSame('', $this->fixture->getTitle());
}
/**
* Test if a title can be set
*
* @test
* @return void
*/
public function aTitleCanBeSet() {
$this->fixture->setTitle('Hello World');
$this->assertSame('Hello World', $this->fixture->getTitle());
}
Objekteigenschaften, die als ObjectStorage dienen, testen wir wie folgt. Dabei testen wir neben den Setter- und Getter-Methoden auch die Möglichkeit, Objekte hinzuzufügen (add/attach) respektive zu löschen (remove/detach).
Im ersten Schritt stellen wir zunächst sicher, dass die Eigenschaft, die als Object Storage dient, korrekt initalisiert wird, sprich ein Objekt der Klasse “Tx_Extbase_Persistence_ObjectStorage” ist und der Objektspeicher leer ist:
/**
* Test if getCategories() returns inital value for object storage
*
* @test
* @return void
**/
public function getCategoriesReturnsInitialValueForCategories() {
$this->assertSame('Tx_Extbase_Persistence_ObjectStorage', get_class($this->fixture->getCategories()));
$this->assertEquals(0, count($this->fixture->getCategories()));
}
Als Nächstes prüfen wir die Setter-Methode. Wichtig ist in diesem Zusammenhang die Verwendung von Mock-Objekten. Nur so lässt sich das TDD-Paradigma “loosely coupled objects”, der locker verbundenen Objekte, umsetzen:
/**
* Test if a category can be set
*
* @test
* @return void
**/
public function aCategoryCanBeSet() {
$mockObjStorage = $this->getMock('Tx_Extbase_Persistence_ObjectStorage', array('contains'));
$mockObjStorage->expects($this->any())->method('contains')->with('foo')->will($this->returnValue(TRUE));
$this->fixture->setCategories($mockObjStorage);
$this->assertTrue($this->fixture->getCategories()->contains('foo'));
}
Zu guter Letzt folgen die Tests für die addCategory()- und removeCategory()-Methoden:
/**
* Test if a category can be added
*
* @test
* @return void
**/
public function aCategoryCanBeAdded() {
$category = new Tx_Cgfaq_Domain_Model_Categories();
$this->fixture->addCategory($category);
$this->assertTrue($this->fixture->getCategories()->contains($category);
/**
* Test if a category can be removed
*
* @test
* @return void
**/
public function aCategoryCanBeRemoved() {
$category = new Tx_Cgfaq_Domain_Model_Categories();
$this->fixture->addCategory($category);
$this->assertEquals(1, count($this->fixture->getCategories()));
$this->fixture->removeCategory($category);
$this->assertFalse($this->fixture->getCategories()->contains($category));
}




