La oss TDD være en enkel app i PHP

I denne opplæringen vil jeg presentere en end-to-end eksempel på en enkel applikasjon - laget strengt med TDD i PHP. Jeg vil gå deg gjennom hvert trinn, en om gangen, mens du forklarer de beslutningene jeg har gjort for å få oppgaven gjort. Eksemplet følger tett TDDs regler: skrive tester, skrive kode, refactor.


Trinn 1 - Introduksjon til TDD og PHPUnit

Testdrevet utvikling (TDD)

TDD er en "test-først" -teknikk for å utvikle og designe programvare. Det er nesten alltid brukt i smidige lag, som er et av hovedverktøyene for fleksibel programvareutvikling. TDD ble først definert og introdusert til det profesjonelle fellesskapet av Kent Beck i 2002. Siden da har det blitt en akseptert - og anbefalt - teknikk i daglig programmering.

TDD har tre kjerne regler:

  1. Du har ikke lov til å skrive noen produksjonskode, hvis det ikke er en feiltest for å garantere det.
  2. Du har ikke lov til å skrive mer av en enhetstest enn det er strengt nødvendig for å gjøre det feil. Ikke kompilering / kjøring svikter.
  3. Du har ikke lov til å skrive mer produksjonskode enn det er strengt nødvendig for å gjøre det mislykkede testpasset.

PHPUnit

PHPUnit er verktøyet som gjør at PHP-programmerere kan utføre enhetstesting, og utøve testdrevet utvikling. Det er et komplett enhetstestingsramme med mocking-støtte. Selv om det er noen alternative valg, er PHPUnit den mest brukte og mest komplette løsningen for PHP i dag.

For å installere PHPUnit kan du enten følge med den forrige opplæringen i vår "TDD i PHP" -session, eller du kan bruke PEAR, som forklart i den offisielle dokumentasjonen:

  • bli til rot eller bruk sudo
  • sørg for at du har den nyeste PEAR: pæreoppgradering PEAR
  • aktiver automatisk oppdagelse: pære config-sett auto_discover 1
  • installer PHPUnit: pære installere pear.phpunit.de/PHPUnit

Mer informasjon og instruksjoner for installering av ekstra PHPUnit-moduler finnes i den offisielle dokumentasjonen.

Noen Linux-distribusjoner tilbyr PHPUnit som en forkompilert pakke, selv om jeg alltid anbefaler en installasjon via PEAR, fordi den sikrer at den nyeste og oppdaterte versjonen er installert og brukt.

NetBeans & PHPUnit

Hvis du er fan av NetBeans, kan du konfigurere den til å fungere med PHPUnit ved å følge disse trinnene:

  • Gå til NetBeans 'konfigurasjon (Verktøy / Valg)
  • Velg PHP / Unit Testing
  • Kontroller at inngangspunkter for "PHPUnit Script" til en gyldig PHPUnit-kjørbar. Hvis det ikke gjør det, vil NetBeans fortelle deg dette, så hvis du ikke ser noen røde merknader på siden, er du god til å gå. Hvis ikke, se etter PHPUnit-kjørbar på systemet ditt og skriv inn banen i feltet. For Linux-systemer er denne banen vanligvis / Usr / bin / PHPUnit.

Hvis du ikke bruker en IDE med støtte for enhetstesting, kan du alltid kjøre testen direkte fra konsollen:

 cd / min / applikasjoner / test / folder phpunit

Trinn 2 - Problemet med å løse

Vårt team har til oppgave å gjennomføre en "word wrap" -funksjon.

La oss anta at vi er en del av et stort selskap, som har et sofistikert program for å utvikle og vedlikeholde. Vårt team har til oppgave å gjennomføre en "word wrap" -funksjon. Våre kunder ønsker ikke å se horisontale rullefelt, og det er ute jobb å overholde.

I så fall må vi lage en klasse som er i stand til å formatere en vilkårlig tekstbit som er gitt som input. Resultatet skal være innpakket på et bestemt antall tegn. Reglene for ordinnpakning bør følge oppførselen til andre daglige applikasjoner, som tekstredigerere, tekstområdene på nettsiden etc. Vår klient forstår ikke alle ordlighetsregler, men de vet at de vil ha det, og de vet det bør fungere på samme måte som de har opplevd i andre apper.


Trinn 3 - Planlegging

TDD hjelper deg med å oppnå bedre design, men det eliminerer ikke behovet for design og tenkning på forhånd.

En av tingene som mange programmerere glemmer, etter at de har startet TDD, er å tenke og planlegge på forhånd. TDD hjelper deg med å oppnå et bedre design mesteparten av tiden, med mindre kode og verifisert funksjonalitet, men det eliminerer ikke behovet for opprørende design og menneskelig tenkning.

Hver gang du trenger å løse et problem, bør du sette tid til å tenke på det, for å forestille deg en liten design - ikke noe fancy - men nok til å komme i gang. Denne delen av jobben hjelper deg også til å forestille deg og gjette mulige scenarier for logikken i applikasjonen.

La oss tenke på de grunnleggende reglene for en ordbinding. Jeg antar at noen upakket tekst vil bli gitt til oss. Vi vil vite antall tegn per linje, og vi vil ønske at den skal pakkes inn. Så det første som kommer til hjernen min er at hvis teksten har flere tegn enn tallet på en linje, bør vi legge til en ny linje i stedet for det siste romkarakter som fremdeles er på linjen.

Ok, det vil oppsummere oppførselen til systemet, men det er altfor komplisert for enhver test. For eksempel, hva med når et enkelt ord er lengre enn antall tegn som er tillatt på en linje? Hmmm ... dette ser ut som en kantkasse; Vi kan ikke erstatte et mellomrom med en ny linje siden vi ikke har mellomrom på den linjen. Vi burde tvinge ordet, splitte det effektivt i to.

Disse ideene skal være klare nok til det punktet at vi kan begynne programmering. Vi trenger et prosjekt og en klasse. La oss kalle det wrapper.


Trinn 4 - Starte prosjektet og opprette første test

La oss lage vårt prosjekt. Det bør være en hovedmappe for kildeklasser, og a tester / mappe, naturlig, for testene.

Den første filen vi skal lage er en test i tester mappe. All vår fremtidige test vil bli inkludert i denne mappen, så jeg vil ikke spesifisere det eksplisitt igjen i denne opplæringen. Benytt testklassen noe beskrivende, men enkelt. WrapperTest vil gjøre for nå; vår første test ser noe ut som dette:

 require_once dirname (__ FILE__). '/ ... /Wrapper.php'; klasse WrapperTest utvider PHPUnit_Framework_TestCase funksjon testCanCreateAWrapper () $ wrapper = ny Wrapper (); 

Huske! Vi har ikke lov til å skrive noen produksjonskode før en feiltesting - ikke engang en klassedeklarasjon! Derfor skrev jeg den første enkle testen ovenfor, kalt canCreateAWrapper. Noen anser dette trinnet ubrukelig, men jeg anser det som en fin mulighet til å tenke på klassen vi skal skape. Trenger vi en klasse? Hva skal vi kalle det? Skal det være statisk?

Når du kjører testen ovenfor, vil du motta en Fatal Error-melding, som følgende:

 PHP Fatal feil: require_once (): Mislykket åpning kreves '/ path / to / WordWrapPHP / Tests / ... /Wrapper.php' (include_path = '.: / Usr / share / php5: / usr / share / php') i / sti / til / WordWrapPHP / Tests / WrapperTest.php på linje 3

Yikes! Vi bør gjøre noe med det. Opprett en tom wrapper klasse i prosjektets hovedmappe.

 klasse Wrapper 

Det er det. Hvis du kjører testen igjen, passerer den. Gratulerer med din første test!


Trinn 5 - Den første virkelige testen

Så vi har satt opp prosjektet vårt. nå må vi tenke på vår første ekte test.

Hva ville være den enkleste ... den dumeste ... den mest grunnleggende testen som ville gjøre vår nåværende produksjonskode svikt? Vel, det første som kommer til å tenke er "Gi det et kort nok ord, og forvent at resultatet blir uendret."Dette høres mulig, la oss skrive testen.

 require_once dirname (__ FILE__). '/ ... /Wrapper.php'; klasse WrapperTest utvider PHPUnit_Framework_TestCase funksjon testDoesNotWrapAShorterThanMaxCharsWord () $ wrapper = ny Wrapper (); assertEquals ('word', $ wrapper-> wrap ('word', 5)); 

Det ser ganske komplisert ut. Hva betyr "MaxChars" i funksjonsnavnet? Hva gjør 5 i pakke inn metode refererer til?

Jeg tror noe er ikke helt her. Er det ikke en enklere test som vi kan løpe? Ja, det er sikkert! Hva om vi pakker inn ... ingenting - en tom streng? Det høres bra ut. Slett den kompliserte testen ovenfor, og legg i stedet vår nye enklere, som vist nedenfor:

 require_once dirname (__ FILE__). '/ ... /Wrapper.php'; klasse WrapperTest utvider PHPUnit_Framework_TestCase funksjon testItShouldWrapAnEmptyString () $ wrapper = ny Wrapper (); $ this-> assertEquals (", $ wrapper-> wrap (")); 

Dette er mye bedre. Navnet på testen er lett å forstå, vi har ingen magiske strenger eller tall, og mest av alt, IT FAILS!

 Fatal feil: Ring til udefinert metode Wrapper :: wrap () i ... 

Som du kan observere, slettet jeg vår aller første test. Det er ubrukelig å eksplisitt sjekke om et objekt kan initialiseres, når andre tester også trenger det. Dette er normalt. Med tiden vil du oppdage at sletting av tester er en vanlig ting. Test, spesielt enhetstester, må løpe fort - veldig fort ... og ofte - veldig ofte. Med tanke på dette er eliminering av redundans i tester viktig. Tenk deg at du løper tusenvis av tester hver gang du sparer prosjektet. Det bør ta ikke mer enn et par minutter, maksimalt, for dem å løpe. Så vær ikke redd for å slette en test, om nødvendig.

Kom tilbake til produksjonskoden vår, la oss få den testen til å passere:

klasse Wrapper funksjonspakke ($ tekst) return; 

Ovenfor har vi lagt til absolutt ikke mer kode enn det er nødvendig for å gjøre testen bestått.


Trinn 6 - Trykk på På

Nå, for neste feiltest:

 funksjon testItDoesNotWrapAShortEnoughWord () $ wrapper = ny Wrapper (); $ this-> assertEquals ('word', $ wrapper-> wrap ('word', 5)); 

Feilmelding:

Mislyktes hevdet at null matcher forventet "ord".

Og koden som gjør det passerer:

 funksjonspakke ($ tekst) return $ text; 

Wow! Det var lett, var det ikke?

Mens vi er i grønt, vær oppmerksom på at vår testkode kan begynne å rote. Vi må refactor noen få ting. Husk: alltid refactor når testene dine passerer; Dette er den eneste måten du kan være sikker på at du har refactored riktig.

Først, la oss fjerne duplisering av initialiseringen av wrapper-objektet. Vi kan bare gjøre dette en gang i SETUP () metode, og bruk den til begge tester.

klasse WrapperTest utvider PHPUnit_Framework_TestCase private $ wrapper; funksjon setUp () $ this-> wrapper = ny Wrapper ();  funksjonstestItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap ("));  funksjonstestItDoesNotWrapAShortEnoughWord () $ this-> assertEquals ('word', $ this-> wrapper-> wrap ('word', 5)); 

De oppsett Metoden vil løpe før hver ny test.

Deretter er det noen tvetydige biter i den andre testen. Hva er 'ord'? Hva er '5'? La oss gjøre det klart, slik at neste programmerer som leser disse testene ikke trenger å gjette.

Glem aldri at testene dine også er den mest oppdaterte dokumentasjonen for koden din.

En annen programmerer bør kunne lese testene like enkelt som de ville lese dokumentasjonen.

 funksjon testItDoesNotWrapAShortEnoughWord () $ textToBeParsed = 'word'; $ maxLineLength = 5; $ this-> assertEquals ($ textToBeParsed, $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength)); 

Les nå denne påstanden igjen. Leser det ikke bedre? Selvfølgelig gjør det det. Ikke vær redd for lange variabelnavn for dine tester; Auto-fullføring er din venn! Det er bedre å være så beskrivende som mulig.

Nå, for neste feiltest:

 funksjon testItWrapsAWordLongerThanLineLength () $ textToBeParsed = 'alongword'; $ maxLineLength = 5; $ this-> assertEquals ("along \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength)); 

Og koden som gjør det passerer:

 funksjonspakke ($ tekst, $ lineLength) if (strlen ($ text)> $ lineLength) returnere substr ($ text, 0, $ lineLength). "\ n". substr ($ tekst, $ lineLength); returnere $ tekst; 

Det er den åpenbare koden for å gjøre vår siste testpass. Men vær forsiktig - det er også koden som gjør vår første test til ikke passere!

Vi har to alternativer for å fikse dette problemet:

  • endre koden - gjør den andre parameteren valgfri
  • endre den første testen - og ring den til koden med en parameter

Hvis du velger det første alternativet, gjør parameteren valgfri, vil det oppvise et lite problem med gjeldende kode. En valgfri parameter blir også initialisert med en standardverdi. Hva kan en slik verdi være? Null kan høres logisk, men det ville innebære å skrive kode bare for å behandle det spesielle tilfellet. Setter et veldig stort antall, slik at den første hvis uttalelse ville ikke resultere i sant kan være en annen løsning. Men hva er det tallet? Er det 10? Er det 10000? Er det 10000000? Vi kan egentlig ikke si.

Tatt i betraktning alle disse, vil jeg bare endre den første testen:

 funksjonstestItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (", 0)); 

Igjen, alle grønne. Vi kan nå gå videre til neste test. La oss sørge for at hvis vi har et veldig langt ord, vil det vikle på flere linjer.

 funksjonstestItWrapsAWordSeveralTimesIfItsTooLong () $ textToBeParsed = 'averyverylongword'; $ maxLineLength = 5; $ this-> assertEquals ("avery \ nveryl \ nongwo \ nrd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength)); 

Dette mislykkes tydeligvis, fordi vår faktiske produksjonskode wraps bare én gang.

 Mislyktes hevdet at to strenger er like. --- Forventet +++ Faktisk @@ @@ 'avery -veryl -ongwo -rd' + verylongword '

Kan du lukte på samtidig som sløyfe kommer? Vel, tenk igjen. Er en samtidig som sløyfe den enkleste koden som ville få testen til å passere?

Ifølge "Transformation Priorities" (av Robert C. Martin), er det ikke. Rekursjon er alltid enklere enn en loop og det er mye mer testbar.

 funksjonspakke ($ tekst, $ lineLength) if (strlen ($ text)> $ lineLength) returnere substr ($ text, 0, $ lineLength). "\ n". $ this-> wrap (substr ($ tekst, $ lineLength), $ lineLength); returnere $ tekst; 

Kan du til og med se endringen? Det var en enkel en. Alt vi gjorde var, i stedet for sammenkobling med resten av strengen, konkluderer vi med returverdien av å kalle oss med resten av strengen. Perfekt!


Trinn 7 - Bare to ord

Den neste enkleste testen? Hva med to ord kan vikle når det er plass på slutten av linjen.

 funksjon testItWrapsTwoWordsWhenSpaceAtTheEndOfLine () $ textToBeParsed = 'ordord'; $ maxLineLength = 5; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength)); 

Det passer fint. Løsningen kan imidlertid bli litt vanskeligere denne gangen.

I begynnelsen kan du referere til a str_replace () å kvitte seg med plassen og sette inn en ny linje. Ikke; den veien fører til en blindgyde.

Det nest mest åpenbare valget ville være en hvis uttalelse. Noe sånt som dette:

 funksjonspakke ($ tekst, $ lineLength) hvis (strpos ($ text, ") == $ lineLength) returnere substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ tekst, strpos ($ text, ") + 1), $ lineLength), hvis (strlen ($ text)> $ lineLength) returnere substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ tekst, $ lineLength), $ lineLength), returner $ text;

Det kommer imidlertid inn i en endeløs sløyfe, noe som vil føre til at testene feiler ut.

PHP Fatal feil: Tillatt minnestørrelse på 134217728 bytes utmattet

Denne gangen må vi tenke! Problemet er at vår første test har en tekst med en lengde på null. Også, strpos () returnerer falsk når den ikke finner strengen. Å sammenligne false med null ... er? Det er ekte. Dette er dårlig for oss fordi sløyfen vil bli uendelig. Løsningen? La oss endre den første tilstanden. I stedet for å søke etter et mellomrom og sammenligne posisjonen med linjens lengde, må vi i stedet forsøke å ta tegnet direkte i posisjonen som er angitt av linjens lengde. Vi skal gjøre en substr () bare ett tegn langt, med utgangspunkt i akkurat det rette stedet i teksten.

 funksjonskurs ($ tekst, $ lineLength) if (substr ($ text, $ lineLength - 1, 1) == ") returnere substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ tekst, strpos ($ text, ") + 1), $ lineLength), hvis (strlen ($ text)> $ lineLength) returnere substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ tekst, $ lineLength), $ lineLength), returner $ text;

Men, hva hvis plassen ikke er riktig på slutten av linjen?

 funksjon testItWrapsTwoWordsWhenLineEndIsAfterFirstWord () $ textToBeParsed = 'ordord'; $ maxLineLength = 7; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength)); 

Hmm ... vi må revidere våre forhold igjen. Jeg tenker at vi etter alt trenger det søket etter plasskarakterens plassering.

 Funksjonstast ($ tekst, $ lineLength) if (strlen ($ text)> $ lineLength) hvis (strpos (substr ($ text, 0, $ lineLength), ")! = 0) returner substr , strpos ($ text, ")). "\ n". $ this-> wrap (substr ($ tekst, strpos ($ text, ") + 1), $ lineLength), returner substr ($ text, 0, $ lineLength)." \ n ". ($ tekst, $ lineLength), $ lineLength); returner $ text;

Wow! Det fungerer faktisk. Vi flyttet den første tilstanden inne i den andre, slik at vi unngår den endeløse løkken, og vi la til søket etter plass. Likevel, det ser ganske stygg ut. Nestede forhold? Æsj. Det er på tide for noen refactoring.

 funksjonspakke ($ tekst, $ lineLength) if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strpos($text,")) . "\n" . $this->wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength), returner substr ($ text, 0, $ lineLength) $ lineLength), $ lineLength);

Det er bedre bedre.


Trinn 8 - Hva om flere ord?

Ingenting dårlig kan skje som et resultat av å skrive en test.

Den neste enkleste testen ville være å ha tre ord innpakning på tre linjer. Men den testen passerer. Skal du skrive en test når du vet det vil passere? Mesteparten av tiden, nei. Men hvis du er i tvil, eller du kan forestille deg åpenbare endringer i koden som ville få den nye testen til å mislykkes, og de andre passerer, så skriv den! Ingenting dårlig kan skje som et resultat av å skrive en test. Vurder også at tester er dokumentasjonen din. Hvis testen representerer en viktig del av logikken, skriver du den!

Videre er det faktum at testene vi kom opp med, å passere, en indikasjon på at vi nærmer oss en løsning. Selvfølgelig, når du har en arbeidsalgoritme, vil enhver test som vi skriver passere.

Nå - tre ord på to linjer med linjen som slutter inne i det siste ordet; nå, det mislykkes.

 funksjon testItWraps3WordsOn2Lines () $ textToBeParsed = 'ord ord ord'; $ maxLineLength = 12; $ this-> assertEquals ("word word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength)); 

Jeg ventet nesten denne til å jobbe. Når vi undersøker feilen, får vi:

Mislyktes hevdet at to strenger er like. --- Forventet +++ Faktisk @@ @@-ordord-ord '+' ord + ordord '

Jepp. Vi burde vikle på den høyeste plassen i en linje.

 funksjonspakke ($ tekst, $ lineLength) if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strrpos($text,")) . "\n" . $this->Wrap (substr ($ text, strrpos ($ text, ") + 1), $ lineLength), returner substr ($ text, 0, $ lineLength) $ lineLength), $ lineLength);

Bare erstatt strpos () med strrpos () innenfor den andre hvis uttalelse.


Trinn 9 - Andre sviktende tester? Edge Cases?

Ting blir vanskeligere. Det er ganske vanskelig å finne en sviktende test ... eller en prøve, for den saks skyld, det var ikke skrevet ennå.

Dette er en indikasjon på at vi er ganske nær en endelig løsning. Men hei, jeg tenkte bare på en test som vil mislykkes!

 funksjon testItWraps2WordsOn3Lines () $ textToBeParsed = 'ordord'; $ maxLineLength = 3; $ this-> assertEquals ("wor \ nd \ nwor \ nd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength)); 

Men jeg tok feil. Det går. Hmm ... Er vi ferdige? Vente! Hva med denne?

 funksjon testItWraps2WordsAtBoundry () $ textToBeParsed = 'ordord'; $ maxLineLength = 4; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength)); 

Det mislykkes! Utmerket. Når linjen har samme lengde som ordet, vil vi at den andre linjen ikke skal begynne med et mellomrom.

Mislyktes hevdet at to strenger er like. --- Forventet +++ Faktisk @@ @@ 'ord-ord' + wor + d '

Det finnes flere løsninger. Vi kunne introdusere en annen hvis uttalelse for å sjekke om startplassen. Det ville passe inn med resten av betingelsene som vi har opprettet. Men er det ikke en enklere løsning? Hva om vi bare trim () teksten?

 funksjonspakke ($ tekst, $ lineLength) $ text = trim ($ text); hvis (strlen ($ tekst) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strrpos($text,")) . "\n" . $this->Wrap (substr ($ text, strrpos ($ text, ") + 1), $ lineLength), returner substr ($ text, 0, $ lineLength) $ lineLength), $ lineLength);

Der går vi.


Trinn 10 - Vi er ferdige

På dette punktet kan jeg ikke finne noen feil test å skrive. Vi må være ferdige! Vi har nå brukt TDD til å bygge en enkelt, men nyttig, seks-linjers algoritme.

Noen ord om å stoppe og "bli ferdig." Hvis du bruker TDD, tvinger du deg til å tenke på alle slags situasjoner. Deretter skriver du tester for de situasjonene, og i prosessen begynner å forstå problemet mye bedre. Denne prosessen resulterer vanligvis i en intim kunnskap om algoritmen. Hvis du ikke kan tenke på noen andre sviktende tester for å skrive, betyr dette at algoritmen din er perfekt? Ikke nødvendig, med mindre det er et forhåndsdefinert sett med regler. TDD garanterer ikke feilkode det hjelper deg bare med å skrive bedre kode som bedre forstås og endres.

Enda bedre, hvis du oppdager en feil, er det så mye lettere å skrive en test som gjengir feilen. På denne måten kan du sikre at feilen aldri oppstår igjen - fordi du har testet for det!


Endelige notater

Du kan hevde at denne prosessen ikke er teknisk "TDD". Og du har rett! Dette eksempelet er nærmere hvor mange daglige programmerere som jobber. Hvis du vil ha en ekte "TDD som du mener det" eksempel, vennligst legg igjen en kommentar nedenfor, og jeg planlegger å skrive en i fremtiden.

Takk for at du leste!