Scrivere test con SimpleTest

Download the SimpleTest testing framework - Unit tests and mock objects for PHP
1.0.1 release cycle started. Features include include file upload, better PHP5 support for mock objects, and HTML label support. There is also a big clean up of method names and some internals refactoring.

Questo documento presuppone una certa familiarità con il concetto di unit testing e la conoscenza del linguaggio di sviluppo PHP. E' una guida pensata per i nuovi utenti di SimpleTest che si sentano particolarmente impazienti di iniziare. Informazioni più approfondite, specialmente nel caso si sia completamento all'oscuro di unit testing, possono essere reperite nella documentazione successiva; case test di esempio possono essere trovati nel tutorial.

# unit Iniziare subito ad utilizzare il tester

Tra i tool di collaudo lo unit tester è quello più vicino allo sviluppatore. Nel contesto dello sviluppo agile, il codice di test si posiziona accanto al codice sorgente dal momento che entrambi vengono redatti nel medesimo momento. In questa ottica, SimpleTest aspira ad essere una soluzione di test completa per lo sviluppatore PHP ed è chiamato "`Simple"' perché dovrebbe risultare semplice da utilizzare e da estendere (non è stata una scelta di nome così brillante, in effetti). SimpleTest include tutte le classiche funzioni che ci si potrebbe aspettare da un porting di JUnit e di PHPUnit ed include la gestione degli oggetti Mock.

Quel che rende questo tool immediatamente utile allo sviluppatore PHP è il suo web brower interno. Questo, infatti, permette di sviluppare test in grado di navigare siti web, compilare form e collaudare le pagine. Il fatto di poter scrivere i test in PHP significa che risulta molto semplice sviluppare test integrati.

Un esempio può essere quello di collaudare il fatto che, dopo la registrazione sul sito, i dati dell'utente vengano effettivamente scritti sul database.

Il modo più rapido per dimostrare il funzionamento di SimpleTest è tramite un esempio.

Supponiamo di voler collaudare una semplice classe per il log su file chiamata `Log` e memorizzata in classes/log.php. Iniziamo col creare uno script di test che chiameremo tests/log_test.php e che popoleremo come segue:

<?php <strong>require_once('simpletest/autorun.php');</strong> require_once('../classes/log.php'); class TestOfLogging extends <strong>UnitTestCase</strong> { } ?> simpletest può essere una directory locale o una dell'include path. E' necessario modificare il path in funzione della posizione in cui si è installato SimpleTest. Il file "autorun.php" fa molto più che includere i file di SimpleTest: è lo stesso, difatti, che esegue anche i vari test.

`TestOfLogging` è il nostro primo test case e, al momento, è vuoto. Ogni case test è una classe che estende una delle classi base di SimpleTest: è possibile avere quanti case test si desidera all'interno dello stesso file.

Con sole tre linee per definire l'infrastruttura e per l'inclusione della classe `Log` abbiamo ottenuto una suite di test. Tuttavia, non ci sono ancora test da eseguire.

Per il nostro primo test assumeremo che la classe `Log` accetti il nome del file da scrivere come parametro del costruttore e che esista una directory temporanea nella quale scrivere questo file. <?php require_once('simpletest/autorun.php'); require_once('../classes/log.php'); class TestOfLogging extends UnitTestCase { function <strong>testLogCreatesNewFileOnFirstMessage()</strong> { @unlink('/temp/test.log'); $log = new Log('/temp/test.log'); <strong>$this->assertFalse(file_exists('/temp/test.log'));</strong> $log->message('Should write this to a file'); <strong>$this->assertTrue(file_exists('/temp/test.log'));</strong> } } ?> Quando un test case viene eseguito, vengono cercati ed eseguiti i metodi il cui nome inizi con la stringa "test". Qualsiasi metodo abbia un nome che non inizi per "test" non è un test.

Si noti la lunghezza del nome `testLogCreatesNewFileOnFirstMessage()`: l'uso di nomi lunghi è considerata una buona pratica perché rende l'output del test più leggibile.

Normalmente si desidererà avere più di un test test in un test case ma parleremo di questo in seguito.

Le assertion all'interno dei metodi di test generano dei messaggi indirizzati al framework il quale mostra immediatamente a schermo il risultato.

Il fatto che la risposta sia immediata è importante non soltanto nel caso in cui il codice generi un crash ma anche perché, in questo modo, le funzioni di stampa possono visualizzare il contenuto di debug accanto al messaggio di assertion interessato.

Per visualizzare il risultato non si dovrà far altro che eseguire il test. Non è necessario altro codice: è sufficiente aprire la pagina con un browser.

Nel caso che il test fallisca, il risultato avrà il seguente aspetto:

# TestOfLogging

Fail: testLogCreatesNewFileOnFirstMessage->True assertion failed.
1/1 test cases complete. 1 passes and 1 fails. Nel caso il test passi con successo questo:

# TestOfLogging

1/1 test cases complete. 2 passes and 0 fails. Ed infine, il caso in cui si otteniene un risultato di questo tipo 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 indica che la classe classes/Log.php non può essere trovata. La classe dovrebbe essere qualcosa del tipo:

<?php<strong> class Log { function Log($file_path) { } function message() { } }</strong> ?>

Scrivere il codice dopo aver scritto il test è divertente. Molto più che divertente: questo sistema si chiama "Test Driven Development".

Per maggiori informazioni su `UnitTestCase`, si veda la documentazione sugli unit test.

# group Raggruppare i test

E' improbabile che in un'applicazione reale venga avviato un solo case test e per questo deve esistere un sistema per raggruppare i case in test script che, all'occorrenza, eseguano più test per l'intera applicazione.

Il primo passo da eseguire è creare un nuovo file chiamato tests/all_tests.php e popolarlo con il seguente codice: <?php <strong>require_once('simpletest/autorun.php');</strong> class AllTests extends <strong>TestSuite</strong> { function AllTests() { $this->TestSuite(<strong>'All tests'</strong>); <strong>$this->addFile('log_test.php');</strong> } } ?> L'inclusione di "autorun.php" permette di eseguire, con l'invocazione di questo script, anche gli eventuali test aggiunti in futuro.

Il metodo `TestSuite::addFile()` include il test case specificato e vi recupera tutte le nuove classi discendenti da `SimpleTestCase`. `UnitTestCase` è solo un esempio di classe derivata da `SimpleTestCase` ed è possibile, naturalmente, crearne altre a piacimento. `TestSuite::addFile()` può essere usato anche per includere altre test suite.

Le classi delle case test non vengono instanziate immediatamente. Quando la test suite viene eseguita, questa costruisce ogni istanza solo al momento in cui viene raggiunto il relativo test e la distrugge immediatamente dopo. Questo significa che il costruttore è invocato prima di ogni esecuzione di test ed il distruttore prima che il successivo test inizi.

E' consetuidine raggruppare il codice dei case test in superclassi la cui esecuzione diretta non è prevista ma che vengono utilizzate come classi base di altri test. Affinché `autorun` funzioni propriamente un test case non dovrebbe ciecamente eseguire alcun altra "`test case extensions"' che non esegua effettivamente alcun test. Questo, infatti, potrebbe essere conteggiato, durante l'esecuzione, come un test eseguito. E' raro che questo rappresenti un problema ma per evitare l'inconveniente è sufficiente marcare la classe base come `abstract`. SimpleTest non esegue classi abstract. Nell'uso con PHP4, la direttiva `SimpleTestOptions::ignore()` in qualche punto del test case sortisce lo stesso effetto.

Inoltre, il file del test case non dovrebbe essere incluso in altri punti del codice altrimenti nessun test case verrà aggiunto al gruppo di test. Questo potrebbe essere un problema più serio del precedente perché se le classi del test case dovessero risultare già caricate dal \texttt{PHP}, il metodo `TestSuite::addFile()` non sarebbe in grado di individuarle.

Per visualizzare i risultato è sufficiente invocare tests/all_tests.php dal web server o dal command line.

Per ulteriori informazioni sulla costruizione di test suite, si vedia la test suite documentation

# mock Usare gli oggetti mock

Facciamo un salto nel futuro ed affrontiamo qualcosa di davvero complesso.

Assumiamo che la nostra classe per il log sia stata collaudata e completata. Supponiamo anche che si stia collaudando un'altra classe a cui sia richiesto di scrivere dei messaggi di log, per esempio `SessionPool`. Vogliamo collaudare un metodo che avrà probabilmente un codice simile a:

<strong> class SessionPool { ... function logIn($username) { ... $this->_log->message("User $username logged in."); ... } ... } </strong> Nello spirito del riutilizzo, riccorriamo alla classe `Log`. Un test convenzionale dovrebbe assomigliare a: <?php require_once('simpletest/autorun.php'); require_once('../classes/log.php'); <strong>require_once('../classes/session_pool.php');</strong> class <strong>TestOfSessionLogging</strong> extends UnitTestCase { function setUp() { <strong>@unlink('/temp/test.log');</strong> } function tearDown() { <strong>@unlink('/temp/test.log');</strong> } function testLoggingInIsLogged() { <strong>$log = new Log('/temp/test.log'); $session_pool = &new SessionPool($log); $session_pool->logIn('fred');</strong> $messages = file('/temp/test.log'); $this->assertEqual($messages[0], "User fred logged in.<strong>\n</strong>"); } } ?> Rimandiamo la spiegazione dei metodi `setUp()` and `tearDown()` a più tardi.

Il design di questo case test non è completamente sbagliato ma potremmo provare a migliorarlo. Stiamo spendendo tempo gingillandoci con i file di log quando questo non sarebbe competenza di questo test. Quel che è successo è che abbiamo un accoppiamento troppo forte tra il nostro test e la classe `Log`. Che accadrebbe se si decidesse di non usare più i file ed usare al loro posto la libreria syslog? Accadrebbe che `TestOfSessionLogging` fallirebbe sempre, anche se la sua funzione non sarebbe quella di collaudare Log.

Il test è anche poco robusto per altri motivi minori. Se ci si fa caso, c'è un carriage return aggiunto al messaggio dell'assert. Viene aggiunto dal logger? Cosa accadrebbe se questo aggiungesse anche un time stamp o altri dati?

La sola cosa che desideriamo davvero collaudare è che un particolare messaggio venga veramente inviato al logger. Potremmo ridurre l'accoppiamento tra le due classi passando una falsa classe di logging che accetti le richieste di registrazione di messaggi per i test ma non esegua alcuna azione. Tuttavia, dovrebbe avere lo stesso identico aspetto della classe originale.

Se il falso oggetto non scrivesse su un file allora potremmo risparmiarci di scrivere il codice per eliminare il file di log prima e dopo ogni test.

Potremmo risparmiare perfino più codice se il falso oggetto eseguisse le assertion per noi.

Troppo bello per essere vero? In realtà è piuttosto semplice scrivere un oggetto di questo tipo: <?php require_once('simpletest/autorun.php'); require_once('../classes/log.php'); require_once('../classes/session_pool.php'); <strong>Mock::generate('Log');</strong> class TestOfSessionLogging extends UnitTestCase { function testLoggingInIsLogged() {<strong> $log = &new MockLog(); $log->expectOnce('message', array('User fred logged in.'));</strong> $session_pool = &new SessionPool(<strong>$log</strong>); $session_pool->logIn('fred'); } } ?> Con l'invocazione di `Mock::generate()` viene generata una nuova classe chiamata `MockLog`. Questa classe appare come un clone della classe originale con la differenza che ci permette di cablarvi dentro del codice di test. E' quello che `expectOnce()` fa: specifica che se il metodo `message()` dovesse essere chiamato, dovrebbe esserlo con il parametro "User fred logged in.".

Il test sarà scatenato quando il codice di `SessionPool::logIn()` invocherà `message()` sull'oggetto `MockLog`. La chiamata al Mock scatenerà una comparazione tra i parametri e restituirà un evento di successo o di fallimento al display del test. E' anche possibile utilizzare dei carateri jolly, così non sarà necessario collaudare ogni singolo parametro di una chiamata se se ne vuole collaudare solo uno.

Se il Mock raggiunge la fine del test case senza che il metodo venga invocato, l'attesa di `expectOnce()` scatenerà un evento di fallimento di test. In altre parole, il Mock è in grado di individuare l'assenza di comportamenti così come la loro prosenza.

Con gli oggetti Mock della suite SimpleTest è possibile definire arbitrariamente gruppi di valori di ritorno, sequenze di ritorno, valori di ritorno dipendenti dagli argomenti, sequenze di parametri attesi e limiti sul numero di invocazioni.

Per ulteriori informazioni su mocking e stubbing si veda la documentazione sugli oggetti mock.

# web Il collaudo di pagine web

Uno dei requisiti delle applicazioni web è la produzione di pagine web. Se stai sviluppando un progetto in top-down e desideri integrare i test durante lo stesura del codice, avrai bisogno di un sistema capace di navigare automaticamente il sito e verificare la correttezza dell'output. Questo lavoro è svolto dal web tester.

Il collaudo web in SimpleTest è abbastanza primitivo dal momento che non suppporta JavaScript. La maggior parte delle altre operazioni del browser sono implementate.

Per dare un'idea, ecco un banale esempio dove viene visitata una "home page" dalla quale si naviga verso la pagina "about" nella quale viene verificata la presenza di determinati contenuti sul client. <?php require_once('simpletest/autorun.php'); <strong>require_once('simpletest/web_tester.php');</strong> class TestOfAbout extends <strong>WebTestCase</strong> { function testOurAboutPageGivesFreeReignToOurEgo() { <strong>$this->get('http://test-server/index.php'); $this->click('About'); $this->assertTitle('About why we are so great'); $this->assertText('We are really great');</strong> } } ?> Con questo codice come acceptance test si può garantire che il contenuto aderisca sempre alle specifiche degli sviluppatori e degli altri stakeholder del progetto.

E' anche possibile navigare i form: <?php require_once('simpletest/autorun.php'); require_once('simpletest/web_tester.php'); class TestOfRankings extends WebTestCase { function testWeAreTopOfGoogle() { $this->get('http://google.com/'); $this->setField('q', 'simpletest'); $this->click("I'm Feeling Lucky"); $this->assertTitle('SimpleTest - Unit Testing for PHP'); } } ?> anche se questo violerebbe i termini e le condizioni di Google(tm)

Per ulteriori informazioni sul web testing, si veda la scriptable browser documentation e la documentazione su WebTestCase.

Iniziare subito ad utilizzare il tester con un esempio Raggruppare i test per collaudare più cose con un solo click Usare gli oggetti mock per facilitare il collaudo e raffinare il controllo Il collaudo di pagine web al livello del browser
Download PHP SimpleTest from SourceForge. The developer's API for SimpleTest gives full detail on the classes and assertions available.
software development, php programming, programming php, software development tools, php tutorial, free php scripts, architecture, php resources, mock objects, junit, php testing, php unit, methodology, test first, sourceforge, open source, unit test, web tester, web testing, html testing tools, testing web pages, php mock objects, navigating websites automatically, automated testing, web scripting, wget, curl testing, jmock for php, jwebunit, phpunit, php unit testing, php web testing, jason sweat, marcus baker, topstyle plug in, phpedit plug in