I denna handledning kommer jag att presentera ett end-to-end exempel på en enkel applikation - gjord strängt med TDD i PHP. Jag kommer att gå igenom varje steg, en i taget, samtidigt som jag förklarar de beslut som jag fattat för att få uppgiften klar. Exemplet följer noga TDD: s regler: skriv tester, skriv kod, refactor.
TDD är en "test-första" teknik för att utveckla och designa programvara. Det används nästan alltid i smidiga lag, vilket är ett av kärnverktygen för flexibel mjukvaruutveckling. TDD definierades först och introducerades av Kent Beck i den professionella gemenskapen 2002. Sedan dess har det blivit en accepterad - och rekommenderad - teknik i vardagsprogrammering.
TDD har tre grundläggande regler:
PHPUnit är verktyget som tillåter PHP-programmerare att utföra enhetsprovning och öva testdriven utveckling. Det är ett komplett enhetstestramverk med stödjande stöd. Även om det finns några alternativa val, är PHPUnit den mest använda och mest kompletta lösningen för PHP idag.
För att installera PHPUnit kan du antingen följa med den tidigare handledningen i vår "TDD i PHP" -session, eller du kan använda PEAR enligt förklaringen i den officiella dokumentationen:
rot
eller använd sudo
päronuppgradering PEAR
päronkonfiguration-set auto_discover 1
päron installera pear.phpunit.de/PHPUnit
Mer information och instruktioner för installation av extra PHPUnit-moduler finns i den officiella dokumentationen.
Vissa Linux-distributioner erbjuder PHPUnit som ett förkompilerat paket, men jag rekommenderar alltid en installation via PEAR, eftersom den säkerställer att den senaste och aktuella versionen installeras och används.
Om du är ett fan av NetBeans kan du konfigurera det för att fungera med PHPUnit genom att följa dessa steg:
Om du inte använder en IDE med stöd för enhetstestning kan du alltid köra ditt test direkt från konsolen:
cd / min / applikationer / test / mapp phpunit
Vårt team har till uppgift att genomföra en "word wrap" -funktion.
Låt oss anta att vi är en del av ett stort företag, som har en sofistikerad applikation att utveckla och underhålla. Vårt team har till uppgift att genomföra en "word wrap" -funktion. Våra kunder vill inte se horisontella rullningsfält, och det är inte bara ett jobb att följa.
I så fall måste vi skapa en klass som kan formatera en godtycklig textbit som tillhandahålls som inmatning. Resultatet ska vara inslaget på ett visst antal tecken. Reglerna för ordförpackning bör följa beteendet hos andra dagliga applikationer, som textredigerare, textsidor på webbsidor etc. Vår kund förstår inte alla ordlighetsregler, men de vet att de vill ha det, och de vet det ska fungera på samma sätt som de har upplevt i andra appar.
TDD hjälper dig att uppnå en bättre design, men det eliminerar inte behovet av avancerad design och tänkande.
En av de saker som många programmerare glömmer, efter att de har startat TDD, är att tänka och planera på förhand. TDD hjälper dig att uppnå en bättre design för det mesta, med mindre kod och verifierad funktionalitet, men det eliminerar inte behovet av avancerad design och mänskligt tänkande.
Varje gång du behöver lösa ett problem, bör du avsätta tid för att tänka på det, föreställa dig en liten design - inget fint - men tillräckligt för att komma igång. Denna del av jobbet hjälper dig också att föreställa dig och gissa möjliga scenarier för programmets logik.
Låt oss tänka på de grundläggande reglerna för en ordförpackningsfunktion. Jag antar att någon obelastad text kommer att ges till oss. Vi kommer att känna till antalet tecken per rad och vi vill att den ska förpackas. Så det första som jag tänker på är att om texten har fler tecken än numret på en rad, borde vi lägga till en ny rad i stället för det sista mellanslag som fortfarande finns på linjen.
Okej, det skulle sammanfatta uppförandet av systemet, men det är alltför komplicerat för något test. Till exempel, hur är det när ett enda ord är längre än antalet tecken som är tillåtna på en rad? Hmmm ... det här ser ut som ett kantfall; Vi kan inte ersätta ett mellanslag med en ny rad eftersom vi inte har några mellanslag på den linjen. Vi bör tvinga ordet och splittra det effektivt i två.
Dessa idéer bör vara tydliga nog så att vi kan börja programmera. Vi behöver ett projekt och en klass. Låt oss kalla det Omslag
.
Låt oss skapa vårt projekt. Det bör finnas en huvudmapp för källklasser och a test /
mapp, naturligtvis, för testen.
Den första filen vi ska skapa är ett test inom tester
mapp. Allt vårt framtida test kommer att finnas i den här mappen, så jag kommer inte att uttryckligen ange det igen i den här handledningen. Namn testklassen något beskrivande, men enkelt. WrapperTest
kommer att göra för nu vårt första test ser något ut så här:
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; klass WrapperTest utökar PHPUnit_Framework_TestCase funktion testCanCreateAWrapper () $ wrapper = new Wrapper ();
Kom ihåg! Vi får inte skriva någon produktionskod före ett felprov - inte ens en klassdeklaration! Därför skrev jag det första enkla testet ovan, kallat canCreateAWrapper
. Vissa anser att detta steg är värdelöst, men jag anser att det är ett bra tillfälle att tänka på den klass vi ska skapa. Behöver vi en klass? Vad ska vi kalla det? Ska det vara statiskt?
När du kör testet ovan kommer du att få ett Fatal Error-meddelande, som följande:
PHP Dödligt fel: require_once (): Misslyckad öppning krävs '/ path / to / WordWrapPHP / Tests / ... /Wrapper.php' (include_path = '.: / Usr / share / php5: / usr / share / php') in / sökväg / till / WordWrapPHP / Test / WrapperTest.php på rad 3
Usch! Vi borde göra något åt det. Skapa en tom Omslag
klass i projektets huvudmapp.
klass Wrapper
Det är allt. Om du kör testet igen, passerar det. Grattis till ditt första test!
Så vi har vårt projekt igång. nu måste vi tänka på vårt första verklig testa.
Vad skulle vara det enklaste ... det dumaste ... det mest grundläggande testet som skulle göra att vår nuvarande produktionskod misslyckas? Tja, det första som kommer att tänka är "Ge det ett tillräckligt kort ord och förvänta dig att resultatet blir oförändrat."Det här låter genomförbart, låt oss skriva testet.
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; klass WrapperTest utökar PHPUnit_Framework_TestCase funktion testDoesNotWrapAShorterThanMaxCharsWord () $ wrapper = new Wrapper (); assertEquals ('word', $ wrapper-> wrap ('word', 5));
Det ser ganska komplicerat ut. Vad betyder "MaxChars" i funktionsnamnet? Vad gör 5
i slå in
metod hänvisar till?
Jag tycker att det inte är rätt här. Finns det inte ett enklare test som vi kan springa? Ja, det är säkert! Vad händer om vi sätter ihop ... ingenting - en tom sträng? Det låter bra. Ta bort det komplicerade testet ovan, och lägg till i stället vår nya enklare, som visas nedan:
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; klass WrapperTest utökar PHPUnit_Framework_TestCase funktion testItShouldWrapAnEmptyString () $ wrapper = new Wrapper (); $ this-> assertEquals (", $ wrapper-> wrap ("));
Detta är mycket bättre. Namnet på testet är lätt att förstå, vi har inga magiska strängar eller siffror, och mest av allt, det misslyckas!
Dödsfall: Ring till odefinierad metod Wrapper :: wrap () in ...
Som du kan observera tog jag bort vårt första test. Det är värdelöst att explicit kontrollera om ett objekt kan initieras, när andra test också behöver det. Det här är normalt. Med tiden kommer du att upptäcka att radering av test är en vanlig sak. Tester, speciellt testsatser, måste springa fort - riktigt snabbt ... och ofta - mycket ofta. Med tanke på detta är eliminering av redundans i test viktiga. Tänk dig att du kör tusentals test varje gång du sparar projektet. Det ska ta högst några minuter, för att de ska springa. Så var inte rädd för att ta bort ett test, om det behövs.
Återgå till vår produktionskod, låt oss göra det testet:
klass Wrapper funktionsomslag ($ text) return;
Ovan har vi lagt till absolut ingen mer kod än vad som krävs för att provet ska passera.
Nu, för nästa misslyckande test:
funktion testItDoesNotWrapAShortEnoughWord () $ wrapper = new Wrapper (); $ this-> assertEquals ('word', $ wrapper-> wrap ('word', 5));
Felmeddelande:
Misslyckades hävdar att null matchar förväntat "ord".
Och koden som gör det passerar:
funktionsomslag ($ text) return $ text;
Wow! Det var lätt, var det inte?
Medan vi är i grön, observera att vår testkod kan börja rotna. Vi behöver refactor några saker. Kom ihåg: alltid refactor när dina test passerar; Det här är det enda sättet att du kan vara säker på att du har refactored korrekt.
Låt oss först ta bort dubbletter av initialiseringen av wrapper-objektet. Vi kan bara göra detta en gång i inrätta()
metod, och använd den för båda provningarna.
klass WrapperTest utökar PHPUnit_Framework_TestCase privat $ wrapper; funktion setUp () $ this-> wrapper = new Wrapper (); funktionstestItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (")); funktion testItDoesNotWrapAShortEnoughWord () $ this-> assertEquals ('word', $ this-> wrapper-> wrap ('word', 5));
De
inrätta
Metoden kommer att köras före varje nytt test.
Därefter finns det några tvetydiga bitar i det andra testet. Vad är "ord"? Vad är '5'? Låt oss klargöra så att nästa programmerare som läser dessa test inte behöver gissa.
Glöm aldrig att dina tester också är den mest uppdaterade dokumentationen för din kod.En annan programmerare bör kunna läsa testen lika enkelt som de skulle läsa dokumentationen.
funktion testItDoesNotWrapAShortEnoughWord () $ textToBeParsed = 'word'; $ maxLineLength = 5; $ this-> assertEquals ($ textToBeParsed, $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Läs nu här påståendet igen. Läser det inte bättre? Det gör det självklart. Var inte rädd för långvariga namn för dina test. auto-completion är din vän! Det är bättre att vara så beskrivande som möjligt.
Nu, för nästa misslyckande test:
funktionstestItWrapsAWordLongerThanLineLength () $ textToBeParsed = 'alongword'; $ maxLineLength = 5; $ this-> assertEquals ("along \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Och koden som gör det passerar:
funktionskrapa ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) returnera substr ($ text, 0, $ lineLength). "\ n". substr ($ text, $ lineLength); returnera $ text;
Det är den uppenbara koden för att göra vårt sista testpass. Men var försiktig - det är också koden som gör vårt första test till inte passera!
Vi har två alternativ för att åtgärda detta problem:
Om du väljer det första alternativet, gör parametern valfri, skulle det visa ett litet problem med den aktuella koden. En valfri parameter initieras också med ett standardvärde. Vad kan ett sådant värde vara? Noll kan låta logiskt, men det skulle innebära att du skriver kod bara för att behandla det speciella fallet. Ställer ett mycket stort antal, så att den första om uttalandet skulle inte resultera i sant kan vara en annan lösning. Men vad är det där numret? Är det 10? Är det 10000? Är det 10000000? Vi kan inte riktigt säga.
Med tanke på alla dessa kommer jag bara att ändra det första testet:
funktionstestItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (", 0));
Återigen, alla gröna. Vi kan nu gå vidare till nästa test. Låt oss försäkra oss om att om vi har ett mycket långt ord kommer det att slingras på flera rader.
funktionstestItWrapsAWordSeveralTimesIfItsTooLong () $ textToBeParsed = 'averyverylongword'; $ maxLineLength = 5; $ this-> assertEquals ("avery \ nveryl \ nongwo \ nrd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Det här uppenbarligen misslyckas, eftersom vår faktiska produktionskod endast bryts en gång.
Misslyckades hävdar att två strängar är lika. --- Förväntad +++ Faktisk @@ @@ 'avery -veryl -onwo-third' + verylongword '
Kan du lukta medan
loop kommer? Tja, tänk igen. Är en medan
loop den enklaste koden som skulle göra testet passerat?
Enligt "Transformation Priorities" (av Robert C. Martin) är det inte. Rekursion är alltid enklare än en slinga och det är mycket mer testbar.
funktionskrapa ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) returnera substr ($ text, 0, $ lineLength). "\ n". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); returnera $ text;
Kan du även upptäcka förändringen? Det var enkelt. Allt vi gjorde var, istället för att sammanfoga med resten av strängen, sammanfogar vi med det återvändande värdet att kalla oss själva med resten av strängen. Perfekt!
Nästa enklaste testet? Vad med två ord kan vikas om det finns ett utrymme i slutet av raden.
funktionstestItWrapsTwoWordsWhenSpaceAtTheEndOfLine () $ textToBeParsed = 'ordord'; $ maxLineLength = 5; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Det passar snyggt. Lösningen kan dock bli lite svårare den här gången.
Först kan du hänvisa till a str_replace ()
att bli av med utrymmet och sätt in en ny linje. Inte; den vägen leder till ett slut.
Det näst uppenbara valet skulle vara en om
påstående. Något som det här:
funktionskrapa ($ text, $ lineLength) if (strpos ($ text, ") == $ lineLength) returnera substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength), om (strlen ($ text)> $ lineLength) returnera subststr ($ text, 0, $ lineLength). \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength), returnera $ text;
Det kommer emellertid in i en ändlös slinga, vilket kommer att orsaka att testen går fel.
PHP Fatal error: Tillåtet minnesstorlek på 134217728 bytes utmattad
Den här gången måste vi tänka! Problemet är att vårt första test har en text med en längd av noll. Också, strpos ()
returnerar falskt när det inte kan hitta strängen. Att jämföra false med noll ... är? Det är Sann
. Det här är dåligt för oss eftersom slingan blir oändlig. Lösningen? Låt oss ändra det första villkoret. I stället för att leta efter ett mellanslag och jämföra sin position med linjens längd, låt oss istället försöka ta tecknet direkt till den position som anges av linjens längd. Vi ska göra en substr ()
bara ett tecken långt, börjar med precis rätt plats i texten.
funktionsomslag ($ text, $ lineLength) om (substr ($ text, $ lineLength - 1, 1) == ") returnera substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength), om (strlen ($ text)> $ lineLength) returnera subststr ($ text, 0, $ lineLength). \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength), returnera $ text;
Men, om rummet inte är rätt i slutet av raden?
funktionstestItWrapsTwoWordsWhenLineEndIsAfterFirstWord () $ textToBeParsed = 'ordord'; $ maxLineLength = 7; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Hmm ... vi måste ompröva våra villkor igen. Jag tänker på att vi trots allt behöver den sökningen efter rymdpersonalens position.
(om text, 0, $ lineLength), ")! = 0) returnera substr ($ text, 0) , strpos ($ text, ")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength), returnera subststr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap ($ text, $ lineLength), $ lineLength); returnera $ text;
Wow! Det fungerar faktiskt. Vi flyttade det första villkoret inuti den andra så att vi undviker den ändlösa slingan, och vi lade till sökandet efter rymden. Ändå ser det ganska ful ut. Nestade förhållanden? Usch. Det är dags för lite refactoring.
funktionsomslag ($ text, $ lineLength) if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strpos($text,")) . "\n" . $this->wrap (subtext ($ text, strpos ($ text, ") + 1), $ lineLength), returnera substr ($ text, 0, $ lineLength) $ lineLength), $ lineLength);
Det är bättre bättre.
Inget dåligt kan hända som ett resultat av att skriva ett test.
Det näst enklaste testet skulle vara att ha tre ord omslag på tre linjer. Men det testet passerar. Ska du skriva ett test när du vet att det kommer att passera? För det mesta, nej. Men om du är osäker eller kan du föreställa dig uppenbara ändringar i koden som skulle göra det nya testet misslyckat och de andra passerar, skriv det! Inget dåligt kan hända som ett resultat av att skriva ett test. Tänk också på att dina tester är din dokumentation. Om ditt test representerar en väsentlig del av din logik skriver du det!
Vidare är det faktum att de tester som vi kommit fram emot är en indikation på att vi kommer nära en lösning. Självklart, när du har en arbetsalgoritm, kommer ett test som vi skriver att passera.
Nu - tre ord på två linjer med linjen som slutar inne i det sista ordet; nu misslyckas det.
funktionstestItWraps3WordsOn2Lines () $ textToBeParsed = 'ordordord'; $ maxLineLength = 12; $ this-> assertEquals ("word word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Jag väntade nästan att den här skulle fungera. När vi undersöker felet får vi:
Misslyckades hävdar att två strängar är lika. --- Förväntat +++ Faktiskt @@ @@-ordord -ord "+" ord + ordord "
Japp. Vi borde sätta i det längsta rummet i en linje.
funktionsomslag ($ text, $ lineLength) if (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strrpos($text,")) . "\n" . $this->($ text, 0, $ lineLength). "\ n". $ this-> wrap (substr ($ text, strrpos ($ text, ") + 1) $ lineLength), $ lineLength);
Byt bara ut strpos ()
med strrpos ()
inuti den andra om
påstående.
Saker blir svårare. Det är ganska svårt att hitta ett misslyckat test ... eller något test, för den delen, det var ännu inte skrivet.
Detta är en indikation på att vi är ganska nära en slutlig lösning. Men hej, jag tänkte bara på ett test som kommer att misslyckas!
funktionstestItWraps2WordsOn3Lines () $ textToBeParsed = 'ordord'; $ maxLineLength = 3; $ this-> assertEquals ("wor \ nd \ nwor \ nd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Men jag hade fel. Det passerar. Hmm ... Är vi färdiga? Vänta! Vad sägs om den här?
funktionstestItWraps2WordsAtBoundry () $ textToBeParsed = 'ordord'; $ maxLineLength = 4; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Det misslyckas! Excellent. När linjen har samma längd som ordet vill vi att den andra raden inte börjar med ett mellanslag.
Misslyckades hävdar att två strängar är lika. --- Förväntat +++ Verkligt @@ @@ 'word -word' + wor + d '
Det finns flera lösningar. Vi kunde introducera en annan om
uttalande för att kontrollera för startutrymme. Det skulle passa in med resten av conditionals som vi har skapat. Men finns det inte en enklare lösning? Vad händer om vi bara trimma()
texten?
funktionsomslag ($ text, $ lineLength) $ text = trim ($ text); om (strlen ($ text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength),") != 0) return substr ($text, 0, strrpos($text,")) . "\n" . $this->($ text, 0, $ lineLength). "\ n". $ this-> wrap (substr ($ text, strrpos ($ text, ") + 1) $ lineLength), $ lineLength);
Där går vi.
Vid denna tidpunkt kan jag inte uppfinna något fel test att skriva. Vi måste göra! Vi har nu använt TDD för att bygga en enkelt, men användbar, sex-radig algoritm.
Några ord på stopp och "görs". Om du använder TDD tvingar du dig själv att tänka på alla möjliga situationer. Därefter skriver du tester för de situationerna och börjar i processen att förstå problemet mycket bättre. Vanligtvis resulterar denna process i en intim kunskap om algoritmen. Om du inte kan tänka på några andra felaktiga tester att skriva, betyder det att din algoritm är perfekt? Ej nödvändigt, såvida det inte finns en fördefinierad uppsättning regler. TDD garanterar inte buggfri kod; det hjälper dig bara att skriva bättre kod som bättre kan förstås och ändras.
Ännu bättre, om du upptäcker en bugg, är det så mycket lättare att skriva ett test som reproducerar buggen. På så vis kan du se till att felet aldrig uppstår igen - för att du har testat det!
Du kan hävda att denna process inte är tekniskt "TDD". Och du har rätt! Detta exempel är närmare hur många dagliga programmerare som arbetar. Om du vill ha en sann "TDD som du menar det" exempel, vänligen lämna en kommentar nedan, och jag ska planera att skriva en i framtiden.
Tack för att du läser!