2011-02-11

TDD utflykt i PHP land, del 5

Jag tror största problemet med typen på punkt är själva konfigurationsaspekten. Hur konfigureras typerna och var finns de sedan lagrade? Jag har ingen aning, så jag gör det som är enklast. Någon talar helt enkelt om för presentatören vilka typer som finns. Typen definieras som en vanlig värdeklass med åtminstone ID och namn.

function testSetWaypointTypeIds() { $waypointTypes = array(new ChildWp_Type(1, 'Type 1'), new ChildWp_Type(2, 'Type 2')); $presenter = new ChildWp_Presenter(); $presenter->setTypes($waypointTypes); $presenter->prepare($this); $this->assertTrue(in_array(1, $this->values['wpTypeIds'])); $this->assertTrue(in_array(2, $this->values['wpTypeIds'])); }

Det första testfallet är enkelt, bara kontrollerar att vektorn $wpTypeIds innehåller ID:na i godtycklig ordning. Lägg märke till att testklassen utgör mallen (istället för den tidigare mockversionen av mallen).

public function prepare($template) { $template->assign('pagetitle', $this->translator->Translate('Add waypoint')); $template->assign('wpDesc', 0); $this->prepareTypes($template); $this->coordinate->prepare($template); } private function prepareTypes($template) { $wpTypeIds = array(); foreach ($this->waypointTypes as $i => $type) { $wpTypeIds[] = $type->getId(); } $template->assign('wpTypeIds', $wpTypeIds); }

Namn på typen behandlas på samma sätt (visas inte här).

Namnet på typen ska också översättas. Här finns det några tänkbara varianter. Ett sätt är att översätta namnet i samband med att värdeobjektet skapas, men det förutsätter att det görs dynamisk. Ett annat sätt är att värdeobjektet själv översätter namnet när getName() anropas. Ett tredje sätt är att presentatören översätter namnet innan det läggs till vektorn. Eftersom den redan har tillgång till ett översättningsobjekt så går jag på den linjen.

Hittills har en del av verifieringen av översättningen gjorts genom att kontrollera att översättningsobjektet faktiskt blir anropat. Fördelen är att det inte går att "fuska" genom att skicka tillbaka rätt sträng från klassen som testas. Nackdelen är att det är lite besvärligt att sätta upp. Så innan jag går vidare med att testa att namnen blir översatta så tänkte jag göra om verifieringen av översättning.

Jag skapar en ny klass för översättning som översätter genom att helt enkelt lägga till strängen ' tr' i slutet på texten som ska översättas och sedan skicka tillbaka den. Översättning antas vara utförd på korrekt sätt om texten som verifieras i testfallet motsvarar den översatta.

function testSetWaypointTypeNames() { $waypointTypes = array(new ChildWp_Type(1, 'Type 1'), new ChildWp_Type(2, 'Type 2')); $presenter = new ChildWp_Presenter(null, $this->translator); $presenter->setTypes($waypointTypes); $presenter->prepare($this); $this->assertTrue(in_array('Type 1 tr', $this->values['wpTypeNames'])); $this->assertTrue(in_array('Type 2 tr', $this->values['wpTypeNames'])); }

Implementationen i presentatören.

private function prepareTypes($template) { $wpTypeIds = array(); $wpTypeNames = array(); foreach ($this->waypointTypes as $i => $type) { $wpTypeIds[] = $type->getId(); $wpTypeNames[] = $this->translator->translate($type->getName()); } $template->assign('wpTypeIds', $wpTypeIds); $template->assign('wpTypeNames', $wpTypeNames); }

Nu är det bara två saker kvar som är relaterad till typen. Först ska ett initialvärde sättas, alltså den typ som förvald. Men vilken typ ska vara förvald? Detta leder oss till nummer två. Vanligtvis brukar den här typen av användargränssnittkontroll ha ett ogiltigt värde vars syfte är att upplysa användaren som att välja ett värde.

Men är detta något som har med presentatören att göra eller är det något som mallen själv ska ansvara för? Om man i mallen skulle ändra till radioknappar så är det uppenbart att det inte ska finnas ett alternativ som säger åt användaren att välja ett värde. Vi vill inte behöva modifiera presentatör bara för att en mindre detalj i användargränssnittet har ändrats. Så därför låter jag mallen själv hantera det. Det enda som presentatören behöver göra är att välja värde '0' som förvalt värde. Detta betyder i sammanhanget att inget är valt och det är därmed upp till mallen att hantera det.

function testNoTypeIsSelected() { $presenter = new ChildWp_Presenter(); $presenter->prepare($this); $this->assertEqual('0', $this->values['wpType']); }

Innan jag går vidare med valideringen så är det dags att snygga till koden i testfallen. Dels ska jag ta bort all användning av MockTemplate. Dels, eftersom det nu finns en speciell klass för översättning, göra en hjälpmetod för skapande av presentatören, som ser att den alltid blir initierad med ett Request-objekt och översättningsobjekt.

Det uppstår ett problem med förändringarna. I vissa testfall initieras Request-objektet indirekt via $_GET['key']. Detta fungerar bara om värdet är satt innan Request-objektet instansieras. Jag är inte speciellt förtjust i sätta värden direkt på $_GET, så en bättre lösning är att lägga till en metod på Request-objektet som låter en sätta ickevaliderade värden. Det finns redan en metod getForValidation(), så det känns naturligt att lägga till setForValidation().

Nu är det bara valideringen kvar. Mycket av grunden till validering lades i del 2, så jag kommer att bygga vidare på det. Eftersom presentatören för koordinat redan har validering så behöver just den biten inte testas speciellt ingående. Det räcker med att säkerställa att den blir anropad.

En punkts beskrivning är kanske lättast att validera. Det är ett textfält som får innehålla godtycklig text utan egentliga begränsningar. Men samtidigt låter det lite farligt, så vi skriver upp på att-göra-listan att även se till att beskrivningen tvättas ren från potentiellt farligt innehåll. Testfallet är gjort med lite klipp och klistra från del 2.

function testDescriptionIsValidated() { $this->request->setForValidation('desc', 'description'); $presenter = $this->createPresenter(); $this->assertFalse($this->request->get('desc')); $presenter->validate(); $this->assertEqual('description', $this->request->get('desc')); }

Första versionen av valideringen.

public function validate() { $this->request->validate('desc', new Validator_AlwaysValid()); }

Validering av koordinaten borde också vara enkel. Men för säkerhets skulle är det bäst med två testfall. Ett med korrekt koordinat och ett med felaktig, bara för att säkerställa att felmeddelandet kommer också.

function testCoordinateIsValidated() { $this->request->setForValidation(Coordinate_Presenter::lat_hem, 'N'); $this->request->setForValidation(Coordinate_Presenter::lat_deg, '10'); $this->request->setForValidation(Coordinate_Presenter::lat_min, '15'); $this->request->setForValidation(Coordinate_Presenter::lon_hem, 'E'); $this->request->setForValidation(Coordinate_Presenter::lon_deg, '20'); $this->request->setForValidation(Coordinate_Presenter::lon_min, '30'); $presenter = $this->createPresenter(); $presenter->validate(); $this->assertEqual('N', $this->request->get(Coordinate_Presenter::lat_hem)); $this->assertEqual('10', $this->request->get(Coordinate_Presenter::lat_deg)); $this->assertEqual('15', $this->request->get(Coordinate_Presenter::lat_min)); $this->assertEqual('E', $this->request->get(Coordinate_Presenter::lon_hem)); $this->assertEqual('20', $this->request->get(Coordinate_Presenter::lon_deg)); $this->assertEqual('30', $this->request->get(Coordinate_Presenter::lon_min)); } function testSetsErrorIfInvalidCoordinate() { $presenter = $this->createPresenter(); $presenter->validate(); $presenter->prepare($this); $this->assertEqual('Invalid coordinate tr', $this->values[Coordinate_Presenter::coord_error]); }

Presentatören bara delegerar till Coordinate_Presenter, så det är inget att visa.

Sist av det som ska valideras är typen. Användaren måste välja en typ och självklart måste det vara en giltig typ. Det blir ett par snarlika test.

function testSetsErrorIfTypeNotChoosen() { $waypointTypes = array(new ChildWp_Type(1, 'Type 1'), new ChildWp_Type(2, 'Type 2')); $presenter = $this->createPresenter(); $presenter->setTypes($waypointTypes); $presenter->validate(); $presenter->prepare($this); $this->assertEqual('Select waypoint type tr', $this->values['wpTypeError']); }

Här behövs det en lite mer komplicerad validator. En som tar in en vektor med giltiga värden. Om presentatören omvandlar listan med typer till en hashtabell, där ID används som nyckel och namn som värde, blir det enkelt att initiera validatorn med de giltiga värdena.

class Validator_Array { private $values; public function __construct($values) { $this->values = $values; } public function isValid($value) { return in_array($value, $this->values); } }

Ändringarna i presentatören är relativt små. Men det är ändå intressant att se hur enkel prepareTypes() blev i och med att en tabell sätts upp i setTypes().

public function setTypes($waypointTypes) { $this->waypointTypes = array(); foreach ($waypointTypes as $type) { $this->waypointTypes[$type->getId()] = $this->translator->translate($type->getName()); } } private function prepareTypes($template) { $template->assign('wpTypeIds', array_keys($this->waypointTypes)); $template->assign('wpTypeNames', $this->waypointTypes); }

Det blir ytterligare ett par testfall som jag inte visar här. Från validate() ska sant eller falskt returneras beroende på om alla fält var giltiga eller inte.

Nu när valideringen är på plats så är det bara till att konstatera att frågetecknet kring validering av cache-ID fortfarande kvarstår. Uppenbart är att, såsom testfallen är skrivna, vi förutsätter att ID:t har blivit validerat. Men om man tittar i koden så ser man att en indirekt validering redan görs. Den görs när vi kontrollerar att cachens finns. Så man kanske skulle kunna använda den som validering?

function testInitValidatesCacheId() { $cacheManager = new MockCache_Manager(); $this->request->setForValidation(ChildWp_Presenter::req_cache_id, '345'); $cacheManager->setReturnValue('exists', true); $cacheManager->expectOnce('exists', array('345')); $cacheManager->setReturnValue('userMayModify', true); $cacheManager->expectOnce('userMayModify', array('345')); $presenter = $this->createPresenter(); $presenter->init($this, $cacheManager); $this->assertEqual('345', $this->request->get(ChildWp_Presenter::req_cache_id)); }

Alltså, om cachen finns så vet vi att ID:t är giltigt.

public function init($template, $cacheManager) { $cacheid = $this->request->getForValidation(self::req_cache_id); if (!$cacheManager->exists($cacheid) || !$cacheManager->userMayModify($cacheid)) $template->error(ERROR_CACHE_NOT_EXISTS); else $this->request->validate(self::req_cache_id, new Validator_AlwaysValid()); }

Om ett eller flera av fälten innehåller ogiltiga värden så visas formuläret igen tillsammans med lämpligt felmeddelande. Den tidigare inmatade information som var giltig ska också visas i formuläret.

function testValidDescriptionIsShownAfterValidate() { $this->request->setForValidation('desc', 'description'); $presenter = $this->createPresenter(); $presenter->validate(); $presenter->prepare($this); $this->assertEqual('description', $this->values['wpDesc']); }

Med det testfallet på plats kan det vara läge att ta itu med "farlig" text i beskrivningsfältet. Tanken med beskrivningen är att kunna ge användaren ytterligare information om punkten. Beskrivningen är renodlad text, så all HTML måste kodas om. Det finns redan en funktion i PHP för detta, så det räcker med att verifiera att den blir anropad.

function testHtmlIsEscapedBeforeAdded() { $this->request->set('cacheid', 2); $this->request->set('wp_type', 1); $this->request->set(Coordinate_Presenter::lat_hem, 'N'); $this->request->set(Coordinate_Presenter::lat_deg, '10'); $this->request->set(Coordinate_Presenter::lat_min, '15'); $this->request->set(Coordinate_Presenter::lon_hem, 'E'); $this->request->set(Coordinate_Presenter::lon_deg, '20'); $this->request->set(Coordinate_Presenter::lon_min, '30'); $this->request->set('desc', 'my & < waypoint'); $childWpHandler = new MockChildWp_Handler(); $childWpHandler->expectOnce('add', array(2, 1, 10.25, 20.5, 'my &amp; &lt; waypoint')); $presenter = $this->createPresenter(); $presenter->addWaypoint($childWpHandler); }

Avslutningsvis blir det lite uppstädning. Det finns ett antal hårdkodade strängar som ska bytas ut mot konstanter i både presentatören och testfallen. Det finns även en del upprepningar i testfallen som kan snyggas till. Men förutom det så känns presentatören färdig. I alla fall så pass färdig att den går att använda för lägga till punkt. Hur det går får vi se i nästa inlägg.

Inga kommentarer: