ExtBase - Tests für Controller schreiben
Heute mal ausnahmsweise auf deutsch, weil ich Angst habe, dass ich mich auf Englisch nicht klar genug ausdrücken kann. Mit diesem Post möchte ich einen Vorschlag machen, wie in ExtBase Tests für Controller geschrieben werden könnten.
Nachdem die Controller Actions quasi die Herzstücke der Anwendung sind - also zumindest die Teile der Anwendung in denen es gilt dass alles andere funktionieren sollte - hätte ich gerne eine Möglichkeit nach anstehenden Refactorings etc. zu überprüfen, ob die Actions noch problemlos ausgeführt werden. Dazu möchte ich so viele Klassen wie möglich aus dem ExtBase Framework mocken - diese sollten ja bereits getestet sein - ich möchte nur wissen, ob meine Businesslogik im Controller noch so funktioniert, wie sie darin implementiert ist.
Die Frage ist jetzt: Warum teste ich nicht einfach meine Businesslogik und lass die Controller ungetestet. Nun - würde ich das so machen, könnte ich zwar nach jedem Refactoring in der Business Logik über Tests prüfen, ob mein Zeugs noch tut, was es soll. Spätestens wenn sich aber Schnittstellen in den Klassen ändern und ich die Tests anpassen muss, bedeuten laufende Tests der Businesslogik aber noch lange nicht, dass die Controller Actions noch funktionieren. Ich hätte zwar 100% getestete Business-Logik, die Anwendung selber würde aber nicht mehr laufen. Darum denke ich, dass es sinnvoll ist, Tests zu schreiben, die überprüfen, ob die Controller noch fehlerfrei arbeiten.
Da im Bereich der Controller-Verarbeitung in ExtBase eine Gewisse Abhängigkeitshölle herrscht (nicht bös gemeint :-) ), scheiterte mein erster Versuch gnadenlos, in dem ich versuchte, einfach den ExtBase Dispatcher auf eine gemockte TS-Konfiguration loszulassen. Das größte Problem dabei war, dass der Dispatcher über eine Konstante feststellt, dass er im Backend aufgerufen wird und damit sämtliche Frontendlogik den Bach hinunter geht. Kurzum: Unmöglich.
Sebastian Kurfürst machte dann den Vorschlag, möglichst nur die eigentliche Action zu testen und alles andere zu mocken. Immer noch nicht ganz so einfach, wie zuerst gedacht, aber hier ist ein Vorschlag, den ich zur Diskussion stellen möchte. Fangen wir mit einer Implementierung des Testcases an:
<?php class Tx_Yag_Controller_GalleryController_testcase extends Tx_Extbase_BaseTestCase { /** * Holds TS configuration of yag extension * @var array */ private $configuration; /** * @var Tx_Yag_Tests_Mocks_GalleryControllerMock */ private $galleryController; /** * @var Tx_Extbase_Dispatcher */ private $dispatcher; /** * Sets up environment for testing gallery controller * * @return void * @author Michael Knoll <mimi@kaktusteam.de> */ public function setUp() { // This is needed for some basic initialization! $this->dispatcher = new Tx_Extbase_Dispatcher(); $this->configuration = Tx_Yag_Tests_Mocks_ConfigurationMocks::getBasicConfiguration(); $this->galleryController = t3lib_div::makeInstance('Tx_Yag_Tests_Mocks_GalleryControllerMock'); $this->galleryController->injectMockView(new Tx_Fluid_View_TemplateView()); $this->galleryController->injectMockRepository(new Tx_Yag_Tests_Mocks_GalleryRepositoryMock()); $this->galleryController->injectSettings($this->configuration['settings']); } /** * Tests index action of gallery controller * @test */ public function indexAction() { $this->galleryController->indexAction(); } /** * Tests show action of gallery controller * @test */ public function showAction() { $this->galleryController->showAction(new Tx_Yag_Domain_Model_Gallery()); } } ?>
Wie man sehen kann, musste ich um den eigentlichen Controller zu testen eine erbende Klasse implementieren, welche mir den Zugriff auf ein paar versteckte Eigenschaften ermöglicht. Hier meine Controller-Mock Klasse:
<?php class Tx_Yag_Tests_Mocks_GalleryControllerMock extends Tx_Yag_Controller_GalleryController { public function injectMockView(Tx_Extbase_MVC_View_ViewInterface $view) { $this->view = $view; } public function injectMockRepository( Tx_Extbase_Persistence_RepositoryInterface $mockRepository) { $this->galleryRepository = $mockRepository; } public function getView() { return $this->view; } } ?>
Zuletzt war es noch nötig, eine eigene Autoloader Datei für die Tests hinzuzufügen, da der Extbase Autoload nur im "Classes" Verzeichnis einer Extension nach Klassendefinitionsdateien sucht:
<?php $extensionTestsPath = t3lib_extMgm::extPath('yag') . 'Tests/'; 'tx_yag_tests_mocks_configurationmocks' => $extensionTestsPath . 'Mocks/ConfigurationMocks.php', 'tx_yag_tests_mocks_gallerycontrollermock' => $extensionTestsPath . 'Mocks/GalleryControllerMock.php', 'tx_yag_tests_mocks_galleryrepositorymock' => $extensionTestsPath . 'Mocks/GalleryRepositoryMock.php' ); ?>
Ich würde dieses Konzept des Testens gerne zur Diskussion stellen und freue mich über Rückmeldungen egal welcher Art. Für mich ist testgetriebene Entwicklung ein recht neues Spielfeld und ich bin mir nicht sicher, ob meine Ideen gut sind oder nicht.
Elbrus S/W
Nachdem ich gerade eben endlich herausgefunden habe, wie man Farbbilder mit Lightroom gescheit in Schwarzweissbilder umwandelt, konnte ich nicht an mich halten und musste ein paar der Elbrus-Bilder vom Fühling 2009 in Schwarzweiss umwandeln... über Kritik und Anregungen freue ich mich wie immer!
Yag 0.0.1 available on Forge
Nach ca. 14-tägiger Entwicklungszeit habe ich heute die neue Gallery-Extension soweit fertiggestellt, dass ich das Forge SVN öffentlich gemacht habe. Damit kann eine erste Vorabversion der Galerie ausgecheckt werden.
Die Version soll in erster Linie als Diskussionsgrundlage dienen, um über die Art der Implementierung in ExtBase sowie das Datenmodell zu diskutieren. Anregungen und Ideen, wie man manche Dinge besser oder anders machen könnte, sind herzlich willkommen.
Das Repository findet sich unter http://forge.typo3.org/repositories/show/extension-yag
Als nächste Schritte möchte ich eine öffentliche Testumgebung einrichten in der die Gallery online getestet werden kann sowie unzählige Tests schreiben, was ich natürlich trotz des guten Vorsatzes bis jetzt noch nicht gemacht habe. Letzteres lag aber auch im Wesentlichen daran, dass ich mich erst mal an das ExtBase Framework rantesten musste, um rauszufinden, wie Tests überhaupt sinnvoll implementiert werden können.
ExtBase Cookbook 7 - Form parameter handling and validation
Today, I tried to handle all form inputs via objects and use validators to check consistency. As this is a common task in web applications, I wanted to share my experiences with you.
After reading all parameters from my form using the request property of my controller, I remembered the way shown in the blog example, using objects to pass form parameters. So what I did was writing objects that had all parameters passed by a form as properties, added getters and setters and used the "object" attribute of the form view helper to make my form use this object for passing parameters. Here's an example of a form object which I saved under "EXT:Classes/Domain/Model/FormObject/AddImagesByPath.php":
class Tx_Yag_Domain_Model_FormObject_AddImagesByPath { protected $basePath; protected $singlesPath; protected $thumbsPath; protected $origsPath; public function getBasePath() { return $this->basePath; } public function getOrigsPath() { return $this->origsPath; } public function getSinglesPath() { return $this->singlesPath; } public function getThumbsPath() { return $this->thumbsPath; } public function setBasePath($basePath) { $this->basePath = $basePath; } public function setOrigsPath($origsPath) { $this->origsPath = $origsPath; } public function setSinglesPath($singlesPath) { $this->singlesPath = $singlesPath; } public function setThumbsPath($thumbsPath) { $this->thumbsPath = $thumbsPath; } }
Here is the snippet of the template where I use the object for passing form-values:
<f:render partial="formErrors" arguments="{for: 'addImagesByPath'}" /> <f:form method="post" controller="AlbumContent" action="addImagesByPath" object="{AddImagesByPath}" name="addImagesByPath" arguments="{gallery : gallery, album : album}"> <f:form.textbox property="basePath" size="30" /><br /> <f:form.textbox property="thumbsPath" size="20" /><br /> <f:form.textbox property="singlesPath" size="20" /><br /> <f:form.submit class="submit" value="Submit"/> </f:form>
The controller action looks like that:
public function addImagesByPathAction( Tx_Yag_Domain_Model_FormObject_AddImagesByPath $addImagesByPath = NULL, Tx_Yag_Domain_Model_Gallery $gallery = NULL, Tx_Yag_Domain_Model_Album $album) { $albumPathConfiguration = Tx_Yag_Lib_AlbumPathConfiguration:: getInstanceByAlbumPathObject($addImagesByPath); $addImagesToAlbumHandler = Tx_Yag_Lib_AddImagesToAlbumHandler:: getInstanceByAlbumAndPathConfiguration( $album, $albumPathConfiguration); $images = $addImagesToAlbumHandler->addImagesFromPathConfiguration(); $this->view->assign('addImagesByPath', $addImagesByPath); $this->view->assign('images', $images); $this->view->assign('gallery', $gallery); $this->view->assign('album', $album); return $this->view->render(); }
For validating my form object, I added a validator in "Classes/Domain/Validator/FormObject/" and named it "AddImagesByPathValidator.php".
Make sure you use the right class name with PEAR convention "path encoding" and you have your validator put into a subdirectory with the same name as the subdirectory in the model directory.
The content of this validator looks like that:
<?php class Tx_Yag_Domain_Validator_FormObject_AddImagesByPathValidator extends Tx_Extbase_Validation_Validator_AbstractValidator { /** * Returns true, if the given AddImagesByPath form object is valid * * @param Tx_Yag_Lib_FormObject_AlbumContent_AddImagesByPath $addImagesByPath The addImagesByPath form object * @return boolean true */ public function isValid($addImagesByPath) { $this->addError( 'The given base path is not a valid path on this system!', 1); return FALSE; } $addImagesByPath->getOrigsPath())) { $this->addError( 'The given origs path is not a valid path inside the given base path!', 2); return FALSE; } $addImagesByPath->getSinglesPath())) { $this->addError( 'The given singles path is not a valid path inside the given base path!', 3); return FALSE; } $addImagesByPath->getThumbsPath())) { $this->addError( 'The given thumbs path is not a valid path inside the given base path!', 4); return FALSE; } return TRUE; } } ?>
Last but not least you can output the error messages with the following two snippets of fluid code. The first part is the inclusion of the errors in your form template, the second is a copied partial for error handling from the blog example.
<f:render partial="formErrors" arguments="{for: 'addImagesByPath'}" />
Erste - letzte Skitour 2009 / 2010
Um nach den anstrengenden Weihnachtsfutterorgien wieder einigermaßen in Form zu kommen, ging's gleich nach den beiden Feiertagen aufs Riedbergerhorn - mit Skiern versteht sich. Schnee, Wetter und Laune waren weit besser als erwartet und ganz nebenbei entstand dieses Panorama aufm Gipfel (da war allerdings aller Schnee weggeblasen...)










