In deze tutorial presenteer ik een end-to-end-voorbeeld van een eenvoudige applicatie - strikt gemaakt met TDD in PHP. Ik zal je stap voor stap doorlopen, terwijl ik de beslissingen uitleg die ik heb gemaakt om de klus te klaren. Het voorbeeld volgt de regels van TDD op de voet: schrijf tests, schrijf code, refactor.
TDD is een "test-eerst" -techniek om software te ontwikkelen en ontwerpen. Het wordt bijna altijd gebruikt in agile teams, omdat het een van de belangrijkste instrumenten is voor agile softwareontwikkeling. TDD werd voor het eerst gedefinieerd en in 2002 door Kent Beck geïntroduceerd in de professionele gemeenschap. Sindsdien is het een geaccepteerde - en aanbevolen - techniek geworden in de dagelijkse programmering.
TDD heeft drie kernregels:
PHPUnit is de tool waarmee PHP-programmeurs eenheidstests kunnen uitvoeren en testgedreven ontwikkeling kunnen oefenen. Het is een compleet unit testing framework met spotondersteuning. Hoewel er een paar alternatieve keuzes zijn, is PHPUnit vandaag de meest gebruikte en meest complete oplossing voor PHP.
Om PHPUnit te installeren, kunt u de vorige zelfstudie volgen in onze "TDD in PHP" -sessie, of u kunt PEAR gebruiken, zoals uitgelegd in de officiële documentatie:
wortel
of gebruik sudo
peer upgrade PEAR
pear config-set auto_discover 1
pear installeer pear.phpunit.de/PHPUnit
Meer informatie en instructies voor het installeren van extra PHPUnit-modules zijn te vinden in de officiële documentatie.
Sommige Linux-distributies bieden PHPUnit als een vooraf samengesteld pakket, hoewel ik altijd een installatie aanbeveel, via PEAR, omdat het ervoor zorgt dat de meest recente en bijgewerkte versie wordt geïnstalleerd en gebruikt.
Als je een fan bent van NetBeans, kun je het configureren om met PHPUnit te werken door deze stappen te volgen:
Als u geen IDE gebruikt met ondersteuning voor unit testing, kunt u uw test altijd rechtstreeks vanuit de console uitvoeren:
cd / my / applications / test / folder phpunit
Ons team is belast met de implementatie van een "word wrap" -functie.
Laten we aannemen dat we deel uitmaken van een grote onderneming, die een geavanceerde toepassing heeft om te ontwikkelen en te onderhouden. Ons team is belast met de implementatie van een "word wrap" -functie. Onze klanten willen geen horizontale schuifbalken zien, en het is de taak om hieraan te voldoen.
In dat geval moeten we een klasse maken die in staat is om een willekeurig stukje tekst op te maken dat als invoer wordt aangeboden. Het resultaat moet een woord zijn dat is omwikkeld met een bepaald aantal tekens. De regels voor het inpakken van woorden moeten het gedrag volgen van andere dagelijkse toepassingen, zoals teksteditors, webpagina-tekstgebieden, enz. Onze klant begrijpt niet alle regels voor het inpakken van woorden, maar ze weten dat ze het willen, en zij weten het zou op dezelfde manier moeten werken als ze in andere apps hebben ervaren.
TDD helpt je om een beter ontwerp te krijgen, maar het maakt de behoefte aan up-front ontwerp en denken niet weg.
Een van de dingen die veel programmeurs vergeten nadat ze TDD hebben gestart, is vooraf denken en plannen. Met TDD kunt u het grootste deel van de tijd een beter ontwerp krijgen, met minder code en geverifieerde functionaliteit, maar dit neemt niet weg dat het ontwerp vooraf en het menselijk denken nodig zijn.
Elke keer dat je een probleem moet oplossen, moet je tijd vrijmaken om erover na te denken, om je een klein ontwerp voor te stellen - niets speciaals - maar genoeg om je op weg te helpen. Dit deel van de taak helpt je ook om mogelijke scenario's voor de logica van de toepassing voor te stellen en te raden.
Laten we eens nadenken over de basisregels voor een functie voor het omzetten van woorden. Ik veronderstel dat er een niet-ingepakte tekst aan ons zal worden gegeven. We zullen het aantal tekens per regel kennen en we willen dat het wordt ingepakt. Dus het eerste dat in me opkomt, is dat als de tekst meer tekens bevat dan het getal op één regel, we een nieuwe regel moeten toevoegen in plaats van het laatste spatiesymbool dat nog steeds op de regel staat.
Oké, dat zou het gedrag van het systeem samenvatten, maar het is veel te gecompliceerd voor elke test. Bijvoorbeeld, als een enkel woord langer is dan het aantal toegestane tekens op een regel? Hmmm ... dit lijkt op een edge-case; we kunnen een spatie niet vervangen door een nieuwe regel omdat we geen spaties op die regel hebben. We moeten het woord afdwingen en het effectief in tweeën splitsen.
Deze ideeën moeten zo duidelijk zijn dat we kunnen beginnen met programmeren. We hebben een project en een klas nodig. Laten we het noemen Wikkel
.
Laten we ons project maken. Er moet een hoofdmap zijn voor bronklassen, en een Tests /
map, natuurlijk, voor de tests.
Het eerste bestand dat we gaan maken is een test binnen de Tests
map. Al onze toekomstige testen zullen in deze map staan, dus ik zal het niet expliciet opnieuw specificeren in deze tutorial. Geef de testklasse een naam die beschrijvend, maar eenvoudig is. WrapperTest
zal het voorlopig doen; onze eerste test ziet er ongeveer zo uit:
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; class WrapperTest breidt PHPUnit_Framework_TestCase uit function testCanCreateAWrapper () $ wrapper = new Wrapper ();
Onthouden! We mogen geen productiecode schrijven voor een mislukte test - zelfs geen klassenverklaring! Dat is waarom ik de eerste eenvoudige test hierboven schreef, genaamd canCreateAWrapper
. Sommigen beschouwen deze stap als nutteloos, maar ik beschouw het als een mooie gelegenheid om na te denken over de klas die we gaan creëren. Hebben we een klas nodig? Hoe moeten we het noemen? Moet het statisch zijn?
Wanneer u de bovenstaande test uitvoert, ontvangt u een bericht 'Fatale fout', zoals de volgende:
PHP Fatale fout: require_once (): Mislukt openen vereist '/ path / to / WordWrapPHP / Tests / ... /Wrapper.php' (include_path = '.: / Usr / share / php5: / usr / share / php') in / pad / naar / WordWrapPHP / Tests / WrapperTest.php op regel 3
Yikes! We zouden er iets aan moeten doen. Maak een leeg Wikkel
klasse in de hoofdmap van het project.
klasse Wrapper
Dat is het. Als u de test opnieuw uitvoert, wordt deze uitgevoerd. Gefeliciteerd met je eerste test!
Dus we hebben ons project opgezet en draaien; nu moeten we nadenken over onze eerste echt test.
Wat zou de eenvoudigste zijn ... de domste ... de meest elementaire test die onze huidige productiecode zou laten mislukken? Nou, het eerste dat in me opkomt, is "Geef het een kort genoeg woord, en verwacht dat het resultaat ongewijzigd blijft."Dit klinkt goed te doen, laten we de test schrijven.
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; class WrapperTest breidt PHPUnit_Framework_TestCase uit function testDoesNotWrapAShorterThanMaxCharsWord () $ wrapper = new Wrapper (); assertEquals ('word', $ wrapper-> wrap ('word', 5));
Dat ziet er vrij ingewikkeld uit. Wat betekent "MaxChars" in de functienaam? Wat doet 5
in de wikkelen
methode verwijzen naar?
Ik denk dat hier iets niet klopt. Is er geen eenvoudiger test die we kunnen uitvoeren? Ja, dat is het zeker! Wat als we verpakken ... niets - een lege string? Dat klinkt goed. Verwijder de gecompliceerde test hierboven en voeg in plaats daarvan onze nieuwe, eenvoudigere toe, die hieronder wordt weergegeven:
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; class WrapperTest breidt PHPUnit_Framework_TestCase uit function testItShouldWrapAnEmptyString () $ wrapper = new Wrapper (); $ this-> assertEquals (", $ wrapper-> wrap ("));
Dit is veel beter. De naam van de test is gemakkelijk te begrijpen, we hebben geen magische reeksen of getallen, en vooral, HET FOUTT!
Fatale fout: Bel naar undefined-methode Wrapper :: wrap () in ...
Zoals je kunt zien, heb ik onze allereerste test verwijderd. Het is nutteloos om expliciet te controleren of een object kan worden geïnitialiseerd, terwijl andere tests het ook nodig hebben. Dit is normaal. Na verloop van tijd zul je merken dat het verwijderen van testen een gebruikelijke zaak is. Tests, vooral unit tests, moeten snel lopen - heel snel ... en vaak - heel vaak. Gezien dit is het elimineren van redundantie in testen belangrijk. Stel je voor dat je elke keer duizenden tests uitvoert als je het project opslaat. Het zou maximaal een paar minuten duren voordat ze kunnen worden uitgevoerd. Wees dus niet doodsbang om een test te verwijderen, indien nodig.
Terugkomend op onze productiecode, laten we die test doorgeven:
class Wrapper function wrap ($ text) return;
Hierboven hebben we absoluut geen code meer toegevoegd dan nodig is om de test te laten slagen.
Nu, voor de volgende mislukte test:
function testItDoesNotWrapAShort EnoughWord () $ wrapper = new Wrapper (); $ this-> assertEquals ('word', $ wrapper-> wrap ('word', 5));
Foutmelding:
Mislukt bewerend dat null overeenkomt met verwacht 'woord'.
En de code waardoor het gaat:
functieomslag ($ tekst) return $ text;
Wauw! Dat was gemakkelijk, was het niet?
Terwijl we in het groen staan, kunnen we vaststellen dat onze testcode kan gaan rotten. We moeten een paar dingen refactoren. Vergeet niet: altijd refactoren wanneer uw tests slagen; dit is de enige manier waarop je er zeker van kunt zijn dat je correct hebt gerefactored.
Laten we eerst de duplicatie van de initialisatie van het wrapper-object verwijderen. We kunnen dit slechts een keer doen in de opstelling()
methode en gebruik deze voor beide tests.
klasse WrapperTest breidt PHPUnit_Framework_TestCase uit private $ wrapper; function setUp () $ this-> wrapper = new Wrapper (); function testItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (")); function testItDoesNotWrapAShortEnoughWord () $ this-> assertEquals ('word', $ this-> wrapper-> wrap ('word', 5));
De
opstelling
methode zal vóór elke nieuwe test worden uitgevoerd.
Vervolgens zijn er een aantal dubbelzinnige stukjes in de tweede test. Wat is 'woord'? Wat is '5'? Laten we het duidelijk maken, zodat de volgende programmeur die deze tests leest, niet hoeft te raden.
Vergeet nooit dat uw tests ook de meest bijgewerkte documentatie voor uw code zijn.Een andere programmeur zou de tests net zo gemakkelijk moeten kunnen lezen als ze de documentatie zouden lezen.
function testItDoesNotWrapAShort EnoughWord () $ textToBeParsed = 'word'; $ maxLineLength = 5; $ this-> assertEquals ($ textToBeParsed, $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Lees deze bewering nogmaals. Klopt dat niet beter? Natuurlijk doet het. Wees niet bang voor lange variabele namen voor uw tests; automatische aanvulling is je vriend! Het is beter om zo beschrijvend mogelijk te zijn.
Nu, voor de volgende mislukte test:
function testItWrapsAWordLongerThanLineLength () $ textToBeParsed = 'alongword'; $ maxLineLength = 5; $ this-> assertEquals ("along \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
En de code waardoor het gaat:
functie wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) retourneert substr ($ text, 0, $ lineLength). "\ n". substr ($ text, $ lineLength); return $ tekst;
Dat is de voor de hand liggende code om onze te maken laatste testpas. Maar wees voorzichtig - het is ook de code die onze eerste test maakt niet voorbijgaan!
We hebben twee opties om dit probleem op te lossen:
Als u de eerste optie kiest, waardoor de parameter optioneel wordt, zou dat een klein probleem vormen met de huidige code. Een optionele parameter wordt ook geïnitialiseerd met een standaardwaarde. Wat zou zo'n waarde kunnen zijn? Zero klinkt misschien logisch, maar het zou betekenen dat je code schrijft om alleen dat speciale geval te behandelen. Het instellen van een zeer groot aantal, zodat de eerste als verklaring zou niet resulteren in waar, kan een andere oplossing zijn. Maar wat is dat nummer? Is het 10? Is het 10000? Is het 10000000? We kunnen niet echt zeggen.
Gezien al deze, zal ik de eerste test aanpassen:
function testItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (", 0));
Nogmaals, allemaal groen. We kunnen nu doorgaan naar de volgende test. Laten we ervoor zorgen dat, als we een heel lang woord hebben, het op verschillende regels zal worden ingepakt.
function testItWrapsAWordSeveralTimesIfItsTooLong () $ textToBeParsed = 'averyverylongword'; $ maxLineLength = 5; $ this-> assertEquals ("avery \ nveryl \ nongwo \ nrd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Dit mislukt uiteraard, omdat onze eigenlijke productiecode maar één keer omloopt.
Mislukt dat twee snaren gelijk zijn. --- Verwacht +++ Actueel @@ @@ 'avery -veryl -ongwo -rd' + verylongword '
Kun je de geur ruiken terwijl
lus komt eraan? Wel, denk opnieuw. Is een terwijl
loop de eenvoudigste code die de test zou doorgeven?
Volgens 'Transformation Priorities' (door Robert C. Martin) is dit dat niet. Recursie is altijd eenvoudiger dan een lus en het is veel meer testbaar.
functie wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) retourneert substr ($ text, 0, $ lineLength). "\ n". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); return $ tekst;
Zie je de verandering zelfs? Het was een simpele. Alles wat we deden was, in plaats van aaneen te rijgen met de rest van de reeks, we concateneren met de retourwaarde van onszelf bellen met de rest van de reeks. Perfect!
De volgende eenvoudigste test? Hoe zit het met twee woorden, wanneer er een spatie aan het einde van de regel is.
function testItWrapsTwoWordsWhenSpaceAtTheEndOfLine () $ textToBeParsed = 'word word'; $ maxLineLength = 5; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Dat past mooi. De oplossing kan deze keer misschien wat lastiger worden.
In eerste instantie kunt u verwijzen naar een str_replace ()
om de ruimte kwijt te raken en een nieuwe regel in te voegen. Niet; die weg loopt dood.
De tweede meest voor de hand liggende keuze zou een zijn als
uitspraak. Iets zoals dit:
functie wrap ($ text, $ lineLength) if (strpos ($ text, ") == $ lineLength) retourneert substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); if (strlen ($ text)> $ lineLength) retourneert substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); return $ text;
Dit komt echter in een eindeloze lus, waardoor de tests fout zullen gaan.
PHP Fatale fout: toegestane geheugengrootte van 134217728 bytes uitgeput
Deze keer moeten we nadenken! Het probleem is dat onze eerste test een tekst heeft met een lengte nul. Ook, strpos ()
geeft false terug als het de string niet kan vinden. Valse vergelijken met nul ... is? Het is waar
. Dit is slecht voor ons omdat de lus oneindig zal worden. De oplossing? Laten we de eerste voorwaarde veranderen. In plaats van naar een spatie te zoeken en de positie ervan te vergelijken met de lengte van de lijn, proberen we in plaats daarvan het personage direct op de positie te nemen die wordt aangegeven door de lengte van de lijn. We zullen a substr ()
slechts één teken lang, beginnend op precies de juiste plek in de tekst.
functie wrap ($ text, $ lineLength) if (substr ($ text, $ lineLength - 1, 1) == ") retourneer substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); if (strlen ($ text)> $ lineLength) retourneert substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); return $ text;
Maar wat als de ruimte aan het einde van de regel niet klopt?
function testItWrapsTwoWordsWhenLineEndIsAfterFirstWord () $ textToBeParsed = 'word word'; $ maxLineLength = 7; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Hmm ... we moeten onze voorwaarden opnieuw herzien. Ik denk dat we tenslotte die zoektocht naar de positie van het ruimtekarakter nodig hebben.
functie wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) if (strpos (substr ($ text, 0, $ lineLength), ")! = 0) geeft substr terug ($ text, 0 , strpos ($ text, ")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); return substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); return $ text;
Wauw! Dat werkt echt. We hebben de eerste voorwaarde in de tweede verplaatst, zodat we de eindeloze lus vermijden en we hebben de zoektocht naar ruimte toegevoegd. Toch ziet het er nogal lelijk uit. Geneste voorwaarden? Bah. Het is tijd voor wat refactoring.
functieomslag ($ text, $ 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); return substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
Dat is beter.
Er kan niets ergs gebeuren als gevolg van het schrijven van een test.
De volgende eenvoudigste test zou zijn om drie woorden te hebben die zich om drie regels wikkelen. Maar die test gaat voorbij. Moet je een test schrijven als je weet dat deze zal slagen? Meestal, nee. Maar als je twijfels hebt, of je kunt je voorstellen dat er duidelijke wijzigingen in de code optreden waardoor de nieuwe test mislukt en de anderen slagen, schrijf het dan! Er kan niets ergs gebeuren als gevolg van het schrijven van een test. Overweeg ook dat uw tests uw documentatie zijn. Als uw test een essentieel onderdeel van uw logica vertegenwoordigt, schrijf deze dan!
Verder is het feit dat de tests die we bedachten voorbijgaan een indicatie dat we dicht bij een oplossing komen. Het is duidelijk dat als je een werkend algoritme hebt, elke test die we schrijven zal slagen.
Nu - drie woorden op twee regels waarvan de regel eindigt in het laatste woord; nu mislukt dat.
function testItWraps3WordsOn2Lines () $ textToBeParsed = 'woordwoordwoord'; $ maxLineLength = 12; $ this-> assertEquals ("word word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Ik verwachtte bijna dat deze zou werken. Wanneer we de fout onderzoeken, krijgen we:
Mislukt dat twee snaren gelijk zijn. --- Verwacht +++ Actueel @@ @@ -'word woord -woord '+' woord + woordwoord '
Yep. We moeten de meest rechtse ruimte in een rij omsluiten.
functieomslag ($ text, $ 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); return substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
Vervang gewoon de strpos ()
met strrpos ()
in de tweede als
uitspraak.
Het wordt steeds lastiger. Het is vrij moeilijk om een falende test te vinden ... of welke test dan ook die nog niet was geschreven.
Dit is een indicatie dat we vrij dicht bij een definitieve oplossing zijn. Maar goed, ik dacht aan een test die mislukt!
function testItWraps2WordsOn3Lines () $ textToBeParsed = 'woordwoord'; $ maxLineLength = 3; $ this-> assertEquals ("wor \ nd \ nwor \ nd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Maar ik zat fout. Het gaat voorbij. Hmm ... zijn we klaar? Wacht! Wat dacht je van deze?
function testItWraps2WordsAtBoundry () $ textToBeParsed = 'woordwoord'; $ maxLineLength = 4; $ this-> assertEquals ("word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Het faalt! Uitstekend. Als de regel even lang is als het woord, willen we dat de tweede regel niet begint met een spatie.
Mislukt dat twee snaren gelijk zijn. --- Verwacht +++ Actueel @@ @@ 'word -woord' + wor + d '
Er zijn verschillende oplossingen. We kunnen een andere introduceren als
verklaring om te controleren op de startruimte. Dat zou passen in de rest van de conditionals die we hebben gemaakt. Maar is er geen eenvoudiger oplossing? Wat als we gewoon trimmen ()
de tekst?
functieomslag ($ text, $ lineLength) $ text = trim ($ text); 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); return substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
Daar gaan we.
Op dit moment kan ik geen falende test uitvinden om te schrijven. We moeten klaar zijn! We hebben nu TDD gebruikt om een eenvoudig, maar handig zesregelig algoritme te maken.
Een paar woorden over stoppen en "klaar zijn". Als je TDD gebruikt, dwing je jezelf om na te denken over allerlei situaties. Je schrijft dan tests voor die situaties en begint het probleem nu veel beter te begrijpen. Gewoonlijk resulteert dit proces in een grondige kennis van het algoritme. Als u geen andere mislukte tests kunt bedenken om te schrijven, betekent dit dan dat uw algoritme perfect is? Niet nodig, tenzij er een vooraf gedefinieerde reeks regels is. TDD staat niet garant voor bug-less code; het helpt je alleen om betere code te schrijven die beter kan worden begrepen en aangepast.
Sterker nog, als je een bug ontdekt, is het zoveel gemakkelijker om een test te schrijven die de bug reproduceert. Op deze manier kunt u ervoor zorgen dat de bug nooit meer voorkomt - omdat u ervoor hebt getest!
Je zou kunnen zeggen dat dit proces technisch gezien niet "TDD" is. En je hebt gelijk! Dit voorbeeld komt dichter bij het aantal dagelijkse programmeurs. Als u een echt "TDD zoals u het wilt" voorbeeld wilt, laat dan hieronder een reactie achter, en ik zal van plan zijn er een te schrijven in de toekomst.
Bedankt voor het lezen!