Tutorial: unit testing con SimpleTest

PHP unit testing tutorial - Creating an example test case in PHP

Nel caso tu sia inesperto di unit testing, la raccomandazione è di provare di volta in volta il codice che si incontrerà durante lettura del tutorial: non richiede molto da scrivere e fornisce la sensazione di quale sia il ritmo della programmazione guidata dal collaudo.

Per eseguire gli esempi è necessaria una directory vuota contenente le directory classes, tests e temp. Decomprimere quindi il framework SimpleTest in tests ed assicurarsi che il web server possa raggiungere le varie posizioni.

# new Un nuovo test case

Nel tutorial rapido è stato mostrato il collaudo di unità applicato ad una semplice classe di log. In questo tutorial su SimpleTest ho intenzione di provare a raccontare l'argomento dello sviluppo della classe nella sua interezza. La classe dell'esempio è piccola e semplice e nel corso dell'introduzione riceverà probabilmente più attenzione di quanta ne riceverebbe realmente in produzione. Ma perfino una classe minuscola come questa richiederà decisioni sul design di soprendente difficoltà.

Può darsi decisioni fin troppo difficili? Piuttosto che tentare di disegnare il tutto fin dall'inizio, si partirà con un requisito noto: il fatto che si vuole scrivere un messaggio su un file. Il messaggio deve essere accodato al file se questo esiste. Successivamente ci si porrà ilproblema di gestire le priorità, i filtraggi etc, ma per adesso posizioneremo il requisito della scrittura del file in cima ai nostri pensieri. Per paura di confonderci non penseremo a nient'altro. Ok, scriviamo un test: <strong><?php require_once(dirname(__FILE__) . '/simpletest/autorun.php'); class TestOfLogging extends UnitTestCase { function testFirstLogMessagesCreatesFileIfNonexistent() { } } ?></strong> Vediamo, passo passo, cosa significa.

Il costrutto `dirname(__FILE__)` si limita a garantire che il path venga interpretato relativamente alla posizione del file corrente.

Cos'è il file autorun.php? Questo file si occupa di rendere disponibile la definizione di `UnitTestCase`. Recupera tutte le classi dei test definite nel file corrente e le esegue automagicamente. Per ottenere questo risultato, imposta un handler di uscita dallo script PHP. Approfondiremo il discorso più tardi quando si affronterà l'argomento della modifica dell'output.

I test stessi sono raccolti in classi di test case. La classe dell'esempio, `TestOfLogging`, estende, come nel caso più tipico, `UnitTestCase`. Quando il test case viene invocato, l'autorunner si metterà alla ricerca dei metodi della classe il cui nome inizi per "test". Ciascuno di questi metodi verrà eseguito nell'ordine in cui è definito nella classe.

L'unico metodo attualmente presente nella classe è `testFirstLogMessagesCreatesFileIfNonexistent()`. Non contiene ancora niente.

Adesso, la definizione vuota del metodo non può eseguire alcunché. Abbiamo bisogno di riempirla con del codice. La classe `UnitTestCase`, normalmente, è pensata per generare durante il suo funzionamento degli eventi che vengono inviati ad un reporter in attesa, attraverso i metodi ereditati da `UnitTestCase`.

Adesso, aggiungiamo il codice di test: <?php require_once(dirname(__FILE__) . '/simpletest/autorun.php'); require_once('../classes/log.php');</strong> class TestOfLogging extends UnitTestCase { function testFirstLogMessagesCreatesFileIfNonexistent() {<strong> @unlink(dirname(__FILE__) . '/../temp/test.log'); $log = new Log(dirname(__FILE__) . '/../temp/test.log'); $log->message('Should write this to a file'); $this->assertTrue(file_exists(dirname(__FILE__) . '/../temp/test.log'));</strong> } } ?>

Starai probabilmente pensando che si tratti di troppo codice per un singolo collaudo e su questo concordo. Non temere. Questo è un costo una tantum a da questo momento in poi i test saranno uno scherzo da aggiungere. E saranno ancora più semplici con i meccanismi che vedremo più avanti.

Può anche darsi che tu abbia pensato che `testFirstLogMessagesCreatesFileIfNonexistent` sia un nome orribile e troppo lungo per un metodo. Generalmente questo sarebbe vero, ma in questo contesto è una buona scelta. Non capiterà mai più di dover digitare nuovamente questa stringa ed il nome lungo permette di risparmiare commenti e specifiche.

Adesso è il momento della nostra prima decisione. Il nostro file si chiama log_test.php (qualsiasi nome sarebbe andato bene) e si trova in una directory chiamata tests (qualsiasi posizione sarebbe andata ugualmente bene). Abbiamo chiamato il file del codice da collaudare log.php e lo abbiamo posizionato in una directory chiamata classes, il che significa che ci apprestiamo a scrivere una classe, ok?

In questo esempio è quello che faremo, ma lo unit tester non è limitato al solo collaudo delle classi. Il fatto è che il codice orientato agli oggetti e semplice da decomporre e da ridisegnare per permettere i collaudi. Non è un caso che lo stile di collaudo granulare degli unit test sia storicamente emerso dalla comunità degli sviluppatori object oriented.

Il test di per se' è minimale. Per prima cosa elimina qualsiasi file di test dovesse essere rimasto da precedenti esecuzioni. Le decisioni di design sopraggiungono rapidamente e tutte insieme.

La nostra classe si chiama `Log` ed accetta il path del file nel costruttore. Noi creiamo un log ed immediatamente gli inviamo un messaggio con il metodo `message()`. Sfortunatamente, la capacità di scegliere nomi originali non è una caratteristica molto richiesta agli sviluppatori software.

L'elemento base di uno unit test è l'assert. Qui si desidera assicurarsi che il log file al quale si è inviato il messaggio venga veramente creato.

`UnitTestCase::assertTrue()` invia un evento di successo se la condizione viene valutata come vera ed un evento di fallimento in caso contrario. Disponiamo di una varietà di differenti assert e di un insieme ancor più grande se estendiamo i nostri test case.

Questa è una lista base:

`assertTrue($x)` | Fallisce se $x è falso
`assertFalse($x)` | Fallisce se $x è vero
`assertNull($x)` | Fallisce se $x è impostato
`assertNotNull($x)` | Fallisce se $x è non impostato
`assertIsA($x, $t)` | Fallisce se $x non è una classe del tipo $t
`assertNotA($x, $t)` | Fallisce se $x è una classe del tipo $t
`assertEqual($x, $y)` | Fallisce se $x == $y è falso
`assertNotEqual($x, $y)` | Fallisce se $x == $y è vero
`assertWithinMargin($x, $y, $m)` | Fallisce se abs($x - $y) < $m è falso
`assertOutsideMargin($x, $y, $m)` | Fallisce se abs($x - $y) < $m è vero
`assertIdentical($x, $y)` | Fallisce se $x == $y è falso or se i tipi differiscono
`assertNotIdentical($x, $y)` | Fallisce se $x == $y è vero ed i tipi sono uguali
`assertReference($x, $y)` | Fallisce a meno che $x e $y siano la stessa variabile
`assertCopy($x, $y)` | Fallisce a meno che $x e $y siano copie identiche
`assertSame($x, $y)` | Fallisce a meno che $x e $y siano lo stesso oggetto
`assertClone($x, $y)` | Fallisce a meno che $x e $y siano identici ma siano oggetti separati
`assertPattern($p, $x)` | Fallisce a meno che l'espressione regolare $p soddisfi $x
`assertNoPattern($p, $x)` | Fallisce se l'espressione regolare $p nonn soddisfi $x
`expectError($x)` | Fallisce se non viene generato un errore che soddisfi il matching con $x
`expectException($x)` | Fallisce se non viene lanciata un'eccezione che soddisfi il matching con $x
`ignoreException($x)` | Ignora qualsiasi eccezione dovesse essere lanciata

Adesso siamo pronti ad eseguire lo script di test puntandovi il browser. Cosa succederà? Dovrebbe essere visualizzato un crash: Fatal error: Failed opening required '../classes/log.php' (include_path='') in /home/marcus/projects/lastcraft/tutorial_tests/Log/tests/log_test.php on line 7 La ragione è che log.php non è ancora stato creato.

Aspetta, ma questo è assurdo! Non starai mica cercando di costruire un collaudo senza ancora avere una sola riga del codice che vuoi collaudare, vero?

# tdd Test Driven Development

Il coinventore di eXtreme Programming, Kent Beck, ha pubblicato un nuovo manifesto. Il libro si intitola Test driven development (TDD) e promuove lo unit testing ad una posizione chiave nel design di software. In breve, l'idea è di scrivere dei piccoli test prima e solo dopo occuparsi della scrittura del codice. Per qualsiasi codice. Fidatevi, funziona.

Scrivi un nuovo test e fai in modo che passi con successo. Quello che otterrai all'inizio è un po' di ridondanza e, generalmente, del codice di qualità schifosa. Assicurandoti che i test continuino a passare ti puoi preoccupare, allora, di ri-arrangiare il codice, cioè di fare il refactoring. Questo ti garantisce di non compromettere le funzionalità. Una volta che il codice è pulito quanto possibile sei pronto ad aggiungere nuove funzionalità, ma non lo farai: aggiungerai, invece, un altro test per la nuova feature e ricomincerai nuovamente il ciclo. Le funzionalità verranno generate attraverso i tentativi di far passare con successo i test che le definiscono.

Immagina il test come una specifica eseguibile che sia stata creata per l'occasione.

E' un approccio radicale e personalmente mi sembra incompleto ma è molto efficace per spiegare cosa sia uno unit tester. Nel nostro caso abbiamo un test che fallisce (per non dire che va in crash) così avremo bisogno di scrivere del codice in log.php: <strong><?php class Log { function __construct($path) { } function message($message) { } } ?></strong> Questo è il minimo da fare per evitare fatal error di PHP. Adesso recuperiamo il risultato:

# TestOfLogging

Fail: testFirstLogMessagesCreatesFileIfNonexistent->True assertion failed.
1/1 test cases complete. 0 passes, 1 fails and 0 exceptions. "TestOfLogging" ha fallito. SimpleTest, di default, utilizza i nomi delle classi per descrivere i test ma è sempre possibile sostituire il nome con uno di nostra scelta: class TestOfLogging extends UnitTestCase { <strong>function __construct() { parent::__construct('Log test'); }</strong> function testFirstLogMessagesCreatesFileIfNonexistent() {<strong> @unlink(dirname(__FILE__) . '/../temp/test.log'); $log = new Log(dirname(__FILE__) . '/../temp/test.log'); $log->message('Should write this to a file'); $this->assertTrue(file_exists(dirname(__FILE__) . '/../temp/test.log'));</strong> } } Il che fornisce:

# Log test

Fail: testFirstLogMessagesCreatesFileIfNonexistent->File created.
1/1 test cases complete. 0 passes, 1 fails and 0 exceptions. Se si ha intenzione di cambiare il nome del test si deve intervenire modificando l'output del reporter, operazione che affronteremo più tardi.

Per far passare il test si potrebbe semplicemente creare il file nel costruttore di `Log`. Questa tecnica d'"inganno" è molto utile per controllare che i test funzionino, a mali estremi. Questo è particolarmente vero nel caso si siano registrati fallimenti dei test e ci si voglia assicurare di non aver dimenticato qualcosa di stupido. Non procederemo così lentamente, perciò: <?php class Log {<strong> var $path;</strong> function __construct($path) {<strong> $this->path = $path;</strong> } function message($message) {<strong> $file = fopen($this->path, 'a'); fwrite($file, $message . "\n"); fclose($file);</strong> } } ?> Ho dovuto incontrare non meno di quattro failure per raggiungere questo passo: non avevo creato la directory temporanea, non l'avevo impostata come pubblicamente accessibile in scrittura, avevo un errore di battitura e mi ero dimenticato di aggiungere la nuova cartella sul CVS (questo l'ho scoperto più tardi). Ognuno di questi problemi avrebbe potuto tenermi occupato per diverse ore se solo si fosse manifestato in un secondo momento, ma è proprio per questo che i test servono.

Con le correzioni necessarie si è ottenuto:

# Log test

1/1 test cases complete. 1 passes, 0 fails and 0 exceptions. Successo!

Può darsi che tu non gradisca lo stile di visualizzazione minimale. Di default, i successi non vengono mostrati perché, generalmente, non c'è bisogno di ulteriori informazioni quando si ha la situazione sotto controllo. Se non si sa come le cose stanno procedendo, allora è il caso di scrivere ulteriori test.

Ok, qui sono stato fin troppo rigido. Se desideri vedere anche i casi di successo allora ti basta estendere la classe `HtmlReporter` ed aggiungerla ai test al posto del reporter standard. Anche io gradisco il comfort, ogni tanto.

# doc I test come documentazione

Adesso una sottigliezza. Non vogliamo che il file creato esista fino a che non inviamo il messaggio. Piuttosto che pensarci troppo sopra, aggiungeremo intanto l'assert: class TestOfLogging extends UnitTestCase { function testFirstLogMessagesCreatesFileIfNonexistent() { @unlink(dirname(__FILE__) . '/../temp/test.log'); $log = new Log(dirname(__FILE__) . '/../temp/test.log');<strong> $this->assertFalse(file_exists(dirname(__FILE__) . '/../temp/test.log'));</strong> $log->message('Should write this to a file'); $this->assertTrue(file_exists(dirname(__FILE__) . '/../temp/test.log')); } } per scoprire che già funzionava così:

# TestOfLogging

1/1 test cases complete. 2 passes, 0 fails and 0 exceptions. Ora, sapevo che avrebbe funzionato. Ho aggiunto il test che lo conferma in parte per mettere in pace l'animo e in parte per documentare questo comportamento. Questa unica linea extra dice molto di più, in questo contesto, di dozzine di righe di use case o di un intero diagramma di attività UML. Che la test suite sia una sorgente di documentazione è un piacevole effetto collaterale di tutti i test.

Dovremmo pulire il file temporaneo alla fine del test? Di solito faccio questa operazione quando ho terminato il metodo di test e vedo che funziona. Non ho voglia di inserire in CVS del codice che lasci in giro residui di file di test dopo i collaudi. Non voglio farlo mentre scrivo i test, comunque. Probabilmente dovrei, ma a volte ho bisogno di vedere come le cose procedono e, poi, è una questione di comodità.

Nella progetti reali si produce più di un test case e, pertanto, il prossimo argomento che dobbiamo trattare è

il raggruppamento dei test in test suite..

Creating a new test case. Test driven development in PHP. Tests as documentation is one of many side effects.
The JUnit FAQ has plenty of useful testing advice. Next is grouping test cases together. You will need the SimpleTest testing framework for these examples.
software development, php programming, programming php, software development tools, php tutorial, free php scripts, architecture, php resources, mock objects, junit, php testing, unit test, automated php testing, test cases tutorial, explain unit test case, unit test example, unit test