itfag | Teknologi. Data. Læring. Deling.

mar/12

19

Enkel PHP templatemotor basert på bufring av utdata – En testdrevet tilnærming

 

Av: Kim Betti, systemutvikler og tidligere student ved HiST

Det er god skikk å separere presentasjon og forretningslogikk. Dersom en klarer å skille disse klart fra hverandre på en ryddig måte oppnår man en del fordeler som vil gjøre koden enklere å videreutvikle og vedlikeholde.

  • Designere som jobber med sidens utseende kan gjøre jobben sin uten å rote i forretningslogikken som ligger bak siden.
  • Logikk og presentasjon kan utvikles uavhengig av hverandre så lenge ikke kravene til hva som skal presenteres endres.
  • Det blir mye enklere å legge til alternative presentasjonsformer. Det er ofte aktuelt å presentere samme data som HTML, PDF, regneark samt en rekke andre forskjellige formater.

Raskt oppsummert: Datamodellen generert av forretningslogikk bør kunne presenteres på forskjellige måter uten at det krever endringer i logikken som genererer dataen. For eksempel: Kode som er ansvarlig for å hente en liste over kalenderavtaler bør ikke vite noe som helst om avtalene den henter skal presentere en liste med avtaler som HTML, RSS eller iCal!

I dette innlegget skal jeg gå gjennom hvordan en kan med en testdreven tilnærming implementere en templatemotor basert på bufring av utdata med en testdreven tilnærming. For å ikke drukne innlegget i kode har jeg bare plukket ut de mest relevante bitene. Resten av koden ligger på GitHub.

Noen få ord om meg – Jeg er utdannet treårig dataingeniør ved Høgskolen i Sør-Trøndelag og jobber nå som systemutvikler i Oslo. Det siste semestret på høgskolen var jeg studieveileder i nettfaget “Webprogrammering med PHP”. Selv om jeg til daglig jobber mest med integrasjon og Java-teknologier var det morsomt å kunne bidra med et PHP innlegg til denne bloggen!

Veldig kort om testdrevet utvikling (TDD)

I testdrevet utvikling legger en vekt på å skrive testene først. Dette tvinger en til å tenke på hva en vil oppnå med koden før en begynner med løsningen. Testdrevet utvikling består av to små steg som vi gjentar om og om igjen.

  • Det første steget er alltid å skrive en test som feiler. Ved å skrive en slik test har vi bestemt hva koden skal gjøre for oss og hvordan grensesnittet skal se ut. Dette betyr ikke at vi nå har fryst programmeringsgrensesnittet for endringer! Testdrevet utvikling legger opp til at en hele tiden forbedrer grensesnittet etterhvert som vi lærer mer om implementasjonen og problemområdet / domenet vi jobber i. Testene vi allerede har skrevet vil passe på at vi ikke bryter kode som har fungert når vi gjør disse endringene.
  • Steg nummer to er å implementere kode som tilfredsstiller testen vi nettopp skrev. Når testen gir oss grønt lys går vi tilbake til punkt nummer en og skriver en ny test som feiler. Se på grønne tester som en tillatelse til å skrive ny kode.

Disse to stegene repeterer vi helt til vi har implementert ønsket funksjonalitet og en pen og vedlikeholdbar implementasjon. Hver gang vi har implementert ny eller refaktorert eksisterende kode passer vi på å kjøre alle testene slik at vi er sikker på at de siste endringene vi gjorde ikke bryter ting som har fungert.

En myte rundt testdrevet utvikling er at det er unødvendig arbeid som reduserer produktiviteten. Dette kunne ikke være lengre fra sannheten! Det tar litt ekstra tid i starten mens man venner seg til denne måten å jobbe på, men jo lengre man praktiserer dette desto større blir utbyttet!

  • Det viser seg at kode som er lett å teste veldig ofte er god kode. Den er ofte veldefinert, gjør ikke mer enn den skal og har få avhengigheter. Det blir rett og slett for mye jobb å skrive kode med unødvendige avhengigheter dersom en skriver testen først.
  • Testene fungerer som dokumentasjon. Testene viser hvordan klasser skal brukes og hvordan de skal oppføre seg. Problemet med kommentarer i kildekode og på papir er at de ikke oppdateres sammen med koden. Disse har en lei tendens til å bli utdatert og dermed lyge om systemet. Kode kan være misledende, men den lyger ikke! Dette betyr også at testkode ikke er annenrangs kode, testkoden er minst like viktig som produksjonskoden og må vedlikeholdes på samme måte!
  • De fanger regresjoner. Dette er en av de største fordelene med enhetstester! I et system med god testdekning kan en trygt gjøre optimaliseringer og legge til ny funksjonalitet uten å være redd for å introdusere feil. Dette er helt uvurderlig i et hvert system av litt størrelse, spesielt for nye utviklere som ikke kjenner systemet!

Det er mye mer til testdrevet utvikling enn hva jeg kan ta for meg i dette innlegget. For alle som holder på med programmering vil jeg på det sterkeste anbefale å lese mer om temaet på nett og i bøker. Bøker og artikler er bra, men det beste en kan gjøre for å lære mer er rett og slett og sette i gang selv!

Templatemotor – Presentasjon av data

Nok introduksjon, la oss begynne med implementeringen av templatemotoren. Som nevn er fokuset i dette innlegget presentasjonsdelen, altså steget der vi tar en datamodell og gjør det om til noe som kan presenteres for en bruker.

Vi begynner med en veldig enkel HTML template som kun består av en HTML tagg og en variabel.

<title><?php echo $title; ?></title>

Nedenfor er et eksempel på en test vi kan skrive. Dette er en relativt stor enhetstest, i praksis skriver en som regel flere og mindre tester, men det blir for omfattende for dette innlegget. Jeg har plukket denne testen fordi den demonstrerer prinsippene godt. Hele testklassen ligger her.

public function testSimpleTemplate() {
    // Oppsett av koden vi vil teste
    $view = new View("data/simple-view.php"); 
    // Sender data inn 
    $view->set("title", "Hello World!"); 
    // Utfører en handling
    $renderedHtml = $view->render();  
    // Verifiserer oppførsel
    $this->assertEquals(
             "<title>Hello World!</title>",
             $renderedHtml); 
}

Alle testmetoder i PHPUnit begynner med prefikset test. Resten av metodenavnet bestemmer vi selv og bør si noe om forventet oppførsel som testes.

Testen ovenfor viser strukturen i en typisk enhetstest. Vi starter ofte med å sette opp grunnlagsdata som brukes til å gjøre noe med systemet før vi verifiserer systemets respons til handlingen vi utførte. De fleste testrammeverk gir støtte for dette gjennom forskjellige assert metoder. Den siste linjen i denne testmetoden verifiserer at teksten "Hello World!" har blitt satt inn i title taggen. Dersom denne antakelsen viser seg å ikke stemme vil testrammeverket feile denne testen.

Legg merke til at vi enda ikke har skrevet kode som faktisk gjør det vi ønsker! Men vi har allerede bestemt en del ting. Vi har for eksempel funnet ut hvordan vi vil at programmeringsgrensesnittet skal se ut. Dersom vi er fornøyd med dette grensesnittet i testen, vil vi forhåpentligvis også være fornøyd med det når vi senere skal ta det i bruk i logikken.

Min personlige erfaring er at det å skrive tester først veldig ofte leder meg til et bra grensesnitt. Vær dog oppmerksom på at det ikke nødvendigvis leder direkte til det “beste” grensesnittet! Det beste grensesnittet er noe en bare kan krype nærmere og nærmere gjennom mange rundturer med testing, implementering og refaktorering!

En annen ting det er verdt å legge merke til er at grensesnittet ikke er spesielt knyttet opp mot HTML. Når (om) det blir aktuelt å legge inn støtte for forskjellige formater vil vi kanskje finne ut at API-et vi har nå er for enkelt. Kanskje vil vi gjøre om klassen View til et grensesnitt (interface) med forskjellige implementasjoner som HtmlView og PDFView..? Kanskje finner vi ut at vi trenger å skille datamodellen fra View i en egen klasse..? Med tester på plass kan vi trygt eksperimentere med forskjellige løsninger uten å være redd for å bryte eksisterende kode så lenge en hele tiden jobber i små inkrementelle steg støttet av tester!

Nedenfor er en metode fra klasen View som testen ovenfor tester. Resten av klassen ligger som vanlig på GitHub.

public function render() {
    // Start output buffering
    ob_start(); 
    // Pakk ut innholdet av arrayet `vars` 
    // slik at de blir tilgjengelig som vanlige variable 
    extract($this->vars); 
    // Importer templaten
    require($this->viewFile); 
    // Returner data vi har fanget 
    return ob_get_clean(); 
}

Dette er et veldig mye brukt mønster i PHP rammeverk. Vanligvis vil all ut-data fra PHP script bli sendt i en datastrøm til brukerens HTTP klient (nettleseren) etterhvert som scriptet kjøres. Det output buffering gjør er å fange ut-data i et buffer i minnet istedenfor å sende det rett ut på nettverket.

Dette trikset kjøper oss en del fleksibilitet på bekostning av litt høyere minnebruk. Med dette på plass blir det mye enklere å implementere funksjonalitet som komprimering, mellomlagring / cache og ikke minst testing!

Lenker:

Dette innlegget har 1 kommentar. Gjerne bidra :-)

Skrevet av: itfag (totalt 65 blogginnlegg)

1 comment

  • blogg.itfag.no/en analyse | Sosiale Medier og sånt · 3. september, 2013, kl. 11:10

    […] og da”, uten at man man går videre og kjøper noe. F.eks. kan man lære å lage en php templatemotor hvis man interesser seg for php programmering. Et annet eksempel er en mini-tipsserie om evernote […]

<<

>>

Theme Design by devolux.nh2.me