In questo tutorial presenterò un esempio end-to-end di una semplice applicazione, realizzata rigorosamente con TDD in PHP. Ti guiderò attraverso ogni passaggio, uno alla volta, mentre spiegherò le decisioni che ho preso per portare a termine il compito. L'esempio segue da vicino le regole del TDD: scrivere test, scrivere codice, refactoring.
TDD è una tecnica "test-first" per sviluppare e progettare software. Viene quasi sempre utilizzato in team agili, essendo uno degli strumenti principali per lo sviluppo di software agile. Il TDD è stato definito e introdotto per la prima volta nella comunità professionale da Kent Beck nel 2002. Da allora, è diventato una tecnica accettata e raccomandata nella programmazione quotidiana.
TDD ha tre regole fondamentali:
PHPUnit è lo strumento che consente ai programmatori PHP di eseguire test unitari e di praticare lo sviluppo basato sui test. Si tratta di un quadro di test unitario completo con supporto di simulazione. Anche se ci sono alcune scelte alternative, PHPUnit è la soluzione più utilizzata e più completa per PHP oggi.
Per installare PHPUnit, puoi seguire il tutorial precedente nella nostra sessione "TDD in PHP", oppure puoi usare PEAR, come spiegato nella documentazione ufficiale:
radice
o usare sudo
pera aggiornamento PEAR
config-set auto_discover 1
pera installa pear.phpunit.de/PHPUnit
Ulteriori informazioni e istruzioni per l'installazione di moduli PHPUnit aggiuntivi sono disponibili nella documentazione ufficiale.
Alcune offerte di distribuzioni Linux PHPUnit come pacchetto precompilato, sebbene raccomandi sempre un'installazione, tramite PEAR, perché assicura che la versione più recente e aggiornata sia installata e utilizzata.
Se sei un fan di NetBeans, puoi configurarlo per lavorare con PHPUnit seguendo questi passaggi:
Se non si utilizza un IDE con supporto per il test delle unità, è sempre possibile eseguire il test direttamente dalla console:
cd / my / applications / test / cartella phpunit
Il nostro team ha il compito di implementare una funzionalità di "word wrap".
Supponiamo di far parte di una grande azienda, che ha una sofisticata applicazione da sviluppare e mantenere. Il nostro team ha il compito di implementare una funzionalità di "word wrap". I nostri clienti non desiderano vedere le barre di scorrimento orizzontali e il loro lavoro è conforme.
In tal caso, dobbiamo creare una classe che sia in grado di formattare un bit di testo arbitrario fornito come input. Il risultato dovrebbe essere racchiuso da una parola a un numero specificato di caratteri. Le regole del word wrapping dovrebbero seguire il comportamento di altre applicazioni di tutti i giorni, come editor di testo, aree di testo di pagine Web, ecc. Il nostro cliente non comprende tutte le regole del word wrapping, ma sa di volerlo e lo sanno dovrebbe funzionare nello stesso modo in cui hanno sperimentato in altre app.
TDD ti aiuta a ottenere un design migliore, ma non elimina la necessità di un design e di un pensiero all'avanguardia.
Una delle cose che molti programmatori dimenticano, dopo aver avviato TDD, è pensare e pianificare in anticipo. TDD ti aiuta a ottenere un design migliore la maggior parte del tempo, con meno codice e funzionalità verificate, ma non elimina la necessità di un design all'avanguardia e del pensiero umano.
Ogni volta che devi risolvere un problema, devi dedicare del tempo a pensarci, immaginare un piccolo disegno - niente di speciale - ma abbastanza per iniziare. Questa parte del lavoro ti aiuta anche a immaginare e indovinare possibili scenari per la logica dell'applicazione.
Pensiamo alle regole di base per una funzione di ritorno a capo. Suppongo che ci sarà dato un testo non-avvolto. Sapremo il numero di caratteri per riga e vorremmo che fosse avvolto. Quindi, la prima cosa che mi viene in mente è che, se il testo ha più caratteri del numero su una riga, dovremmo aggiungere una nuova riga invece dell'ultimo carattere di spazio che è ancora sulla linea.
Ok, questo riassumerebbe il comportamento del sistema, ma è troppo complicato per qualsiasi test. Ad esempio, che dire quando una singola parola è più lunga del numero di caratteri consentiti su una linea? Hmmm ... sembra un caso limite; non possiamo sostituire uno spazio con una nuova linea poiché non abbiamo spazi su quella linea. Dovremmo forzare a racchiudere la parola, dividendola efficacemente in due.
Queste idee dovrebbero essere abbastanza chiare al punto da poter iniziare la programmazione. Avremo bisogno di un progetto e di una lezione. Chiamiamolo involucro
.
Creiamo il nostro progetto. Ci dovrebbe essere una cartella principale per le classi di origine e a test /
cartella, naturalmente, per i test.
Il primo file che creeremo è un test all'interno di test
cartella. Tutti i nostri test futuri saranno contenuti in questa cartella, quindi non lo specificherò di nuovo esplicitamente in questo tutorial. Assegna alla classe di prova un nome descrittivo, ma semplice. WrapperTest
lo farò per ora; il nostro primo test sembra qualcosa del genere:
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; class WrapperTest estende PHPUnit_Framework_TestCase function testCanCreateAWrapper () $ wrapper = new Wrapper ();
Ricorda! Non siamo autorizzati a scrivere alcun codice di produzione prima di un test fallito - nemmeno una dichiarazione di classe! Ecco perché ho scritto il primo semplice test sopra, chiamato canCreateAWrapper
. Alcuni considerano questo passo inutile, ma ritengo che sia una buona opportunità per pensare alla classe che stiamo per creare. Abbiamo bisogno di una lezione? Come dovremmo chiamarlo? Dovrebbe essere statico?
Quando esegui il test sopra riportato, riceverai un messaggio di errore irreversibile, simile al seguente:
Errore fatale PHP: require_once (): apertura fallita richiesta '/ percorso / a / WordWrapPHP / Test / ... /Wrapper.php' (include_path = '.: / Usr / share / php5: / usr / share / php') in / percorso / a / WordWrapPHP / Test / WrapperTest.php sulla riga 3
Yikes! Dovremmo fare qualcosa al riguardo. Crea un vuoto involucro
classe nella cartella principale del progetto.
class Wrapper
Questo è tutto. Se si esegue nuovamente il test, passa. Congratulazioni per il tuo primo test!
Quindi abbiamo il nostro progetto installato e funzionante; ora dobbiamo pensare al nostro primo vero test.
Quale sarebbe il più semplice ... il più stupido ... il test più basilare che farebbe fallire il nostro attuale codice di produzione? Bene, la prima cosa che viene in mente è "Dagli una parola abbastanza breve e aspettati che il risultato sia invariato."Sembra fattibile, scriviamo il test.
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; class WrapperTest estende PHPUnit_Framework_TestCase function testDoesNotWrapAShorterThanMaxCharsWord () $ wrapper = new Wrapper (); assertEquals ('word', $ wrapper-> wrap ('word', 5));
Sembra abbastanza complicato. Cosa significa "MaxChars" nel nome della funzione? Cosa fa 5
nel avvolgere
metodo di riferimento?
Penso che qualcosa non sia proprio qui. Non c'è un test più semplice che possiamo eseguire? Sì, c'è sicuramente! Cosa succede se avvolgere ... nulla - una stringa vuota? Suona bene. Elimina il test complicato sopra, e, invece, aggiungi il nostro nuovo, più semplice, mostrato di seguito:
require_once dirname (__ FILE__). '/ ... /Wrapper.php'; class WrapperTest estende PHPUnit_Framework_TestCase function testItShouldWrapAnEmptyString () $ wrapper = new Wrapper (); $ this-> assertEquals (", $ wrapper-> wrap ("));
Questo è molto meglio. Il nome del test è facile da capire, non abbiamo stringhe o numeri magici e, soprattutto, NON FALLISCE!
Errore irreversibile: chiamata al metodo non definito Wrapper :: wrap () in ...
Come puoi osservare, ho cancellato il nostro primo test. È inutile verificare esplicitamente se un oggetto può essere inizializzato, quando anche altri test ne hanno bisogno. E 'normale. Con il tempo, scoprirai che eliminare i test è una cosa comune. I test, in particolare i test unitari, devono essere eseguiti velocemente - molto velocemente ... e frequentemente - molto frequentemente. Considerando questo, è importante eliminare la ridondanza nei test. Immagina di eseguire migliaia di test ogni volta che salvi il progetto. Dovrebbero essere necessari non più di un paio di minuti, al massimo, per farli funzionare. Quindi, non essere terrorizzato di cancellare un test, se necessario.
Tornando al nostro codice di produzione, facciamo passare questo test:
class Wrapper function wrap ($ text) return;
Sopra, abbiamo aggiunto assolutamente non più codice di quello necessario per fare passare il test.
Ora, per il prossimo test negativo:
function testItDoesNotWrapAShortEnoughWord () $ wrapper = new Wrapper (); $ this-> assertEquals ('word', $ wrapper-> wrap ('word', 5));
Messaggio di errore:
Non è stato possibile affermare che null corrisponde alla "parola" prevista.
E il codice che lo fa passare:
function wrap ($ text) return $ text;
Wow! È stato facile, no??
Mentre siamo nel verde, osserviamo che il nostro codice di prova può cominciare a marcire. Abbiamo bisogno di rifattorizzare alcune cose. Ricorda: sempre refactoring quando passano i test; questo è l'unico modo in cui puoi essere certo che hai refactored correttamente.
Innanzitutto, rimuoviamo la duplicazione dell'inizializzazione dell'oggetto wrapper. Possiamo farlo solo una volta nel impostare()
metodo e usarlo per entrambi i test.
class WrapperTest estende PHPUnit_Framework_TestCase 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));
Il
impostare
il metodo verrà eseguito prima di ogni nuovo test.
Successivamente, ci sono alcuni bit ambigui nel secondo test. Cos'è la parola? Cosa è "5"? Facciamo in modo che il programmatore successivo che legge questi test non debba indovinare.
Non dimenticare mai che i tuoi test sono anche la documentazione più aggiornata per il tuo codice.Un altro programmatore dovrebbe essere in grado di leggere i test con la stessa facilità con cui leggerà la documentazione.
function testItDoesNotWrapAShortEnoughWord () $ textToBeParsed = 'parola'; $ maxLineLength = 5; $ this-> assertEquals ($ textToBeParsed, $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Ora leggi di nuovo questa affermazione. Non leggere meglio? Certo che lo fa. Non abbiate paura dei lunghi nomi di variabili per i vostri test; il completamento automatico è tuo amico! È meglio essere il più descrittivo possibile.
Ora, per il prossimo test negativo:
function testItWrapsAWordLongerThanLineLength () $ textToBeParsed = 'alongword'; $ maxLineLength = 5; $ this-> assertEquals ("along \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
E il codice che lo fa passare:
function wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) restituisce substr ($ text, 0, $ lineLength). "\ n". substr ($ text, $ lineLength); restituire $ testo;
Questo è il codice ovvio per rendere il nostro scorso test pass. Ma attenzione, è anche il codice che fa il nostro primo test non passare!
Abbiamo due opzioni per risolvere questo problema:
Se si sceglie la prima opzione, rendendo il parametro opzionale, questo presenterebbe un piccolo problema con il codice corrente. Anche un parametro opzionale viene inizializzato con un valore predefinito. Quale potrebbe essere un tale valore? Zero potrebbe sembrare logico, ma implicherebbe scrivere codice solo per trattare quel caso speciale. Impostazione di un numero molto grande, in modo che il primo Se la dichiarazione non risulterebbe vera può essere un'altra soluzione. Ma qual è quel numero? È il 10? È 10000? È 10000000? Non possiamo davvero dire.
Considerando tutti questi, modificherò semplicemente il primo test:
function testItShouldWrapAnEmptyString () $ this-> assertEquals (", $ this-> wrapper-> wrap (", 0));
Di nuovo, tutto verde. Ora possiamo passare al test successivo. Facciamo in modo che, se abbiamo una parola molto lunga, si avvolgerà su più righe.
function testItWrapsAWordSeveralTimesIfItsTooLong () $ textToBeParsed = 'averyverylongword'; $ maxLineLength = 5; $ this-> assertEquals ("avery \ nveryl \ nongwo \ nrd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Questo ovviamente fallisce, perché il nostro codice di produzione reale si avvolge solo una volta.
Impossibile affermare che due stringhe siano uguali. --- Previsto +++ Attuale @@ @@ 'avery -veryl -ongwo -rd' + verylongword '
Puoi odorare il mentre
ciclo in arrivo? Bene, ripensateci. È un mentre
loop il codice più semplice che farebbe passare il test?
Secondo 'Transformation Priorities' (di Robert C. Martin), non lo è. La ricorsione è sempre più semplice di un loop ed è molto più verificabile.
function wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) restituisce substr ($ text, 0, $ lineLength). "\ n". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); restituire $ testo;
Riesci anche a individuare il cambiamento? Era semplice. Tutto quello che abbiamo fatto è stato, invece di concatenare con il resto della stringa, concatenare con il valore restituito di chiamare noi stessi con il resto della stringa. Perfezionare!
Il prossimo test più semplice? Che dire di due parole possono avvolgere, quando c'è uno spazio alla fine della linea.
function testItWrapsTwoWordsWhenSpaceAtTheEndOfLine () $ textToBeParsed = 'parola parola'; $ maxLineLength = 5; $ this-> assertEquals ("parola \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Questo si adatta bene. Tuttavia, questa soluzione potrebbe essere un po 'più complicata questa volta.
All'inizio, potresti fare riferimento a a str_replace ()
per sbarazzarsi dello spazio e inserire una nuova linea. non lo fanno; quella strada porta a un vicolo cieco.
La seconda scelta più ovvia sarebbe una Se
dichiarazione. Qualcosa come questo:
function wrap ($ text, $ lineLength) if (strpos ($ text, ") == $ lineLength) restituisce substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength), if (strlen ($ text)> $ lineLength) restituisce substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); restituisce $ text;
Tuttavia, questo entra in un ciclo infinito, che causerà errori di test.
PHP Errore irreversibile: dimensioni di memoria consentite di 134217728 byte esaurite
Questa volta, dobbiamo pensare! Il problema è che il nostro primo test ha un testo con una lunghezza pari a zero. Anche, strpos ()
restituisce false quando non riesce a trovare la stringa. Confrontando il falso con lo zero ... è? È vero
. Questo è male per noi perché il ciclo diventerà infinito. La soluzione? Cambiamo la prima condizione. Invece di cercare uno spazio e confrontarne la posizione con la lunghezza della linea, proviamo invece a prendere direttamente il carattere nella posizione indicata dalla lunghezza della linea. Faremo a substr ()
solo un carattere, iniziando dal punto giusto nel testo.
function wrap ($ text, $ lineLength) if (substr ($ text, $ lineLength - 1, 1) == ") restituisce substr ($ text, 0, strpos ($ text,")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength), if (strlen ($ text)> $ lineLength) restituisce substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); restituisce $ text;
Ma, cosa succede se lo spazio non è proprio alla fine della linea?
function testItWrapsTwoWordsWhenLineEndIsAfterFirstWord () $ textToBeParsed = 'parola parola'; $ maxLineLength = 7; $ this-> assertEquals ("parola \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Hmm ... dobbiamo rivedere le nostre condizioni di nuovo. Penso che dopo tutto avremo bisogno di cercare la posizione del personaggio spaziale.
function wrap ($ text, $ lineLength) if (strlen ($ text)> $ lineLength) if (strpos (substr ($ text, 0, $ lineLength), ")! = 0) restituisce substr ($ text, 0 , strpos ($ text, ")). "\ n". $ this-> wrap (substr ($ text, strpos ($ text, ") + 1), $ lineLength); restituisce substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength); return $ text;
Wow! Funziona davvero. Abbiamo spostato la prima condizione all'interno della seconda in modo da evitare il ciclo infinito e abbiamo aggiunto la ricerca dello spazio. Tuttavia, sembra piuttosto brutto. Condizioni nidificate? Che schifo. È tempo per qualche refactoring.
funzione wrap ($ 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); restituisce substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
Va meglio meglio.
Nulla di male può accadere a seguito della scrittura di un test.
Il prossimo test più semplice sarebbe avere tre parole che si avvolgono su tre righe. Ma quel test passa. Dovresti scrivere un test quando sai che passerà? Il più delle volte, no. Ma, se hai dei dubbi, o puoi immaginare delle ovvie modifiche al codice che farebbero fallire il nuovo test e gli altri passeranno, allora scrivilo! Nulla di male può accadere a seguito della scrittura di un test. Inoltre, considera che i tuoi test sono la tua documentazione. Se il tuo test rappresenta una parte essenziale della tua logica, allora scrivilo!
Inoltre, il fatto che i test a cui siamo arrivati stiano passando indica che ci stiamo avvicinando a una soluzione. Ovviamente, quando si ha un algoritmo funzionante, qualsiasi test che scriviamo passerà.
Ora - tre parole su due righe con la linea che termina all'interno dell'ultima parola; ora, questo fallisce.
function testItWraps3WordsOn2Lines () $ textToBeParsed = 'parola parola parola'; $ maxLineLength = 12; $ this-> assertEquals ("word word \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Quasi mi aspettavo che questo funzionasse. Quando analizziamo l'errore, otteniamo:
Impossibile affermare che due stringhe siano uguali. --- Previsto +++ Actual @@ @@ -'word word -word '+' word + word word '
Sì. Dovremmo avvolgere nello spazio più a destra in una linea.
funzione wrap ($ 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); restituisce substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
Basta sostituire il strpos ()
con strrpos ()
dentro il secondo Se
dichiarazione.
Le cose si stanno facendo più difficili. È abbastanza difficile trovare un test in errore ... o qualsiasi test, peraltro, che non sia stato ancora scritto.
Questo indica che siamo abbastanza vicini a una soluzione finale. Ma, hey, ho appena pensato a un test che fallirà!
function testItWraps2WordsOn3Lines () $ textToBeParsed = 'parola parola'; $ maxLineLength = 3; $ this-> assertEquals ("wor \ nd \ nwor \ nd", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Ma mi sbagliavo. Passa. Hmm ... Abbiamo finito? Aspettare! Che dire di questa?
function testItWraps2WordsAtBoundry () $ textToBeParsed = 'parola parola'; $ maxLineLength = 4; $ this-> assertEquals ("parola \ nword", $ this-> wrapper-> wrap ($ textToBeParsed, $ maxLineLength));
Fallisce! Eccellente. Quando la linea ha la stessa lunghezza della parola, vogliamo che la seconda riga non inizi con uno spazio.
Impossibile affermare che due stringhe siano uguali. --- Previsto +++ Attuale @@ @@ 'parola-parola' + wor + d '
Ci sono diverse soluzioni. Potremmo introdurne un altro Se
dichiarazione per verificare lo spazio iniziale. Ciò si adatterebbe con il resto dei condizionali che abbiamo creato. Ma non c'è una soluzione più semplice? E se solo noi trim ()
il testo?
funzione wrap ($ 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); restituisce substr ($ text, 0, $ lineLength)." \ n ". $ this-> wrap (substr ($ text, $ lineLength), $ lineLength);
Eccoci.
A questo punto, non riesco a inventare alcun test negativo per scrivere. Dobbiamo essere fatti! Ora abbiamo utilizzato TDD per creare un semplice ma utile algoritmo a sei righe.
Qualche parola su come fermarsi e "essere fatto". Se usi TDD, ti costringi a pensare a tutti i tipi di situazioni. Quindi scrivi test per quelle situazioni e, nel processo, inizia a capire il problema molto meglio. Di solito, questo processo si traduce in una conoscenza intima dell'algoritmo. Se non riesci a pensare a nessun altro test fallito da scrivere, significa che il tuo algoritmo è perfetto? Non necessario, a meno che non ci sia un insieme predefinito di regole. TDD non garantisce codice senza errori; semplicemente ti aiuta a scrivere codice migliore che può essere meglio compreso e modificato.
Ancora meglio, se scopri un bug, è molto più facile scrivere un test che riproduca il bug. In questo modo, puoi assicurarti che il bug non si ripeta mai più, perché lo hai testato!
Potresti sostenere che questo processo non è tecnicamente "TDD". E hai ragione! Questo esempio è più vicino a quanti programmatori giornalieri lavorano. Se si desidera un vero esempio di "TDD come si intende", si prega di lasciare un commento qui sotto, e ho intenzione di scriverne uno in futuro.
Grazie per aver letto!