Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

PHPUnit: test e funzioni avanzate

Le organizzazioni degli Unit Test in TestSuite, le fixture e i diversi tipi di test. Il Code coverage e gli oggetti mock.
Le organizzazioni degli Unit Test in TestSuite, le fixture e i diversi tipi di test. Il Code coverage e gli oggetti mock.
Link copiato negli appunti

Ci siamo lasciati lo scorso articolo dopo aver introdotto brevemente la libreria PHPUnit e le sue funzionalità basilari per la scrittura di Unit Test. Fino ad ora però quello che abbiamo spiegato sarebbe stato archiviabile anche attraverso sistemi meno eleganti, sfruttando blocchi di codice di test posti alla fine di ogni file scritto oppure includendo orribili var_dump e print_r all'interno del nostro codice. Ovviamente questo tipo di approccio non è da consigliare, ma posso ben capire chi, per abitudine o per altri motivi a me ignoti, ritenga che i test base possano essere archiviati in questo modo.

Fortunatamente un framework degno di tale nome cerca di fornire molte funzionalità di supporto che facilitino al programmatore il compito per il quale sono stati studiati. PHPUnit non è da meno e, oltre a fornire delle interfacce di base per scrivere Unit Test, implementa anche una serie di funzionalità aggiuntive estremamente utili che aiutano a scrivere test ben ordinati ed assolutamente efficienti. In questo articolo cercherò di elencarle brevemente, soffermandomi con più dettaglio su quelle parti che rendono PHPUnit un framework unico nel panorama PHP e oltre.

Organizzazione degli Unit Test

Prima di iniziare con elencare le funzionalità di supporto è bene tirare le somme di quello che è stato detto la volta scorsa introducendo come PHPUnit consiglia di organizzare i propri Unit Test. Uno degli obiettivi principali di PHPUnit è il fatto che i test siano componibili: un programmatore deve poter riassemblare i test in base alle proprie esigenze, magari perché si trova a dover includere solamente una parte di una libreria in un altro ambiente. Per questo motivo scrivere un unico file contenente tutti i metodi per testare le proprie funzionalità sarebbe un'operazione poco intelligente.

PHPUnit ci viene incontro consigliandoci di organizzare i nostri test in TestSuite. Una TestSuite non è altro che un gruppo di test o altre test suite raggruppate tra loro; in questo modo possiamo organizzare gerarchie complesse di test che possono essere comunque scomposte e riassemblate a piacimento. Una TestSuite funziona concettualmente come se fosse un TestCase con la differenza che non implementa direttamente i test ma necessita che questi vengano registrati manualmente per poi essere eseguiti.

<?php

// ...
$suite = new PHPUnit_Framework_TestSuite( 'My Tests' );

// ad una suite possiamo aggiungere un'altra test suite ...
$suite->addTestSuite( 'Framework_A_Test_Suite' );

// ... oppure direttamente un test case
$suite->addTest( new Framework_A_Test_Case() );

// ...

?>

Fixture

Spesso capita di dover scrivere test che, quando eseguiti, interagiscono, magari in modo distruttivo, con delle fonti di dati. Prendiamo per esempio un'applicazione per la gestione di un database: saranno sicuramente presenti Test Case che controllano il corretto funzionamento delle operazioni di INSERT e di DELETE. In questo caso risulta molto dispendioso in termini di tempo riportare allo stato originale la fonte di dati utilizzata per il test. Questa operazione è chiamata fixture ed è molto comune quando si scrivono dei test. Se il framework non supportasse un sistema che facilita la condivisione del codice di setup e di restore dei dati utilizzati, ci ritroveremmo ad avere test che sono composti in gran parte da codice ripetuto che svolge le stesse operazioni.

PHPUnit permette di implementare nei propri TestCase i metodi setUp e tearDown che devono contenere rispettivamente il codice per preparare l'ambiente corretto per il test ed effettuare eventuale pulizia sugli oggetti interessati. Questi due metodi vengono richiamati automaticamente dal framework ogni volta che viene eseguito un metodo di test. Oltretutto, per assicurarsi che ogni test non sia per nulla influenzato dagli altri, i metodi vengono richiamati ogni volta su nuove istanze della classe che implementa il TestCase. Nel caso fosse necessario condividere alcune operazioni tra test differenti è possibile sfruttare le normali caratteristiche della programmazione ad oggetti e definire una classe comune che implementi le operazioni utili a più test.

Vediamo un esempio:

<?php

require_once 'PHPUnit/Framework.php';

class MyDBTest extends PHPUnit_Framework_TestCase
{
        protected var $connection;

        protected function setUp()
        {
        // Qui effettuiamo una connessione al database utilizzando
                // la nostra libreria preferita
         $this->connection = new DatabaseConnection();
        }
        
        protected function tearDown()
        {
                $this->connection->close();
        }
}

class ATest extends MyDBTest
{
        protected function setUp()
        {
                parent::setUp();
                
        // Aggiungiamo qualche dato da utilizzare per le prove
         $this->connection->insertSomeTestData();
        }
        
        public function testCount()
        {
                $tot = $this->connection->execute( 'SELECT COUNT(*) AS tot from tabella' );
                assertEquals( $tot, 100 );
        }
        
// Eseguiamo altri test ...
}

?>

Testare il sistema di eccezioni

PHPUnit implementa un'estensione della classe base PHPUnit_Framework_TestCase che permette di testare codice che ci si aspetta generi un'eccezione. In questo modo è possibile assicurarsi che un blocco di codice si comporti correttamente anche dal punto di vista del controllo delle operazioni inaspettate.

<?php

require_once 'PHPUnit/Extensions/ExceptionTestCase.php';
 
class ExceptionTest extends PHPUnit_Extensions_ExceptionTestCase
{
    public function testException()
    {
        $this->setExpectedException( 'Exception' );
                
                // Commentare questa riga per far fallire il test
                throw new Exception( 'It works' );
    }
}

?>

Nell'esempio precedente il test verrà eseguito correttamente dato che viene generata l'eccezione richiesta. Se commentassimo invece la parte di codice che si occupa di generare un'eccezione otterremmo un fallimento durante l'esecuzione del test.

Testare la generazione di output

Spesso capita che degli script abbiano come obiettivo quello di generare dell'output da inviare al browser. Utilizzando le asserzioni (i metodi assert*) forniti da PHPUnit nativamente è purtroppo possibile testare solamente che un valore restituito da una chiamata o contenuto in una variabile sia corretto. Per controllare una corretta generazione dell'output è quindi necessario catturare l'output creato dalla funzionalità da testare e valutarlo utilizzando le normali operazioni di assert. Per catturare l'output abbiamo due vie: o implementiamo manualmente il test in modo che sfrutti le funzioni di output buffering per immagazzinare l'output in una variabile, oppure utilizziamo come classe base per i nostri test l'estensione PHPUnit_Extensions_OutputTestCase.

Questa estensione utilizza internamente le funzioni di output buffering per catturare l'output generato e successivamente assicurarsi che abbia il valore corretto:

<?php

require_once 'PHPUnit/Extensions/OutputTestCase.php';
 
class OutputTest extends PHPUnit_Extensions_OutputTestCase
{
        public function testHTMLGeneration()
        {
                $this->expectedOutputString(  '<html><body></body></html>'  );
                
                echo "<html><body></body></html>";
        }
}

?>

Testare le performance dei propri script

Un altro concetto molto importante e legato al discorso del testing è sicuramente quello che riguarda le performance. In molti caso è consigliabile l'utilizzo di un profiler, ma potrebbe capitare che questo non sia a disposizione oppure si vogliano effettuare dei test di performance molto semplici. Anche in questo caso ci viene incontro un'estensione del framework PHPUnit, la classe PHPUnit_Extensions_PerformanceTestCase. Questa classe permette di specificare una soglia di tempo entro la quale è richiesto che il test venga eseguito. Se questa soglia di tempo viene superata viene segnalato un fallimento del test:

<?php

require_once 'PHPUnit/Extensions/PerformanceTestCase.php';
 
class PerformanceTest extends PHPUnit_Extensions_PerformanceTestCase
{
    public function testPerformance()
    {
        $this->setMaxRunningTime(2);
        sleep(1);
    }
}

?>

Conclusione

Chiudiamo per ora con la descrizione delle funzionalità aggiuntive del framework. Nella prossima parte di questo articolo concluderemo l'introduzione a PHPUnit discutendo delle funzionalità più avanzate che riguardano il code coverage e la definizione degli oggetti Mock.

Riprendiamo l'articolo incompleto della settimana scorsa continuando a parlare del framework PHPUnit. In questo ultimo articolo sullo Unit Testing analizzeremo le funzionalità più avanzate fornite dal framework come quelle per la scrittura di oggetti Mock e l'analisi della parte di codice coperta dai propri test. Queste funzionalità sono molto utili e spesso è bene utilizzarle anche in situazioni abbastanza semplici in modo da avere test completi ed efficaci.

Code Coverage

Un test per essere considerato completo deve essere in grado di assicurare chi lo esegue che ogni funzionalità sia stata testata correttamente. Spesso però il problema non è scrivere test che eseguano ogni singola funzionalità implementata e ne controllino il corretto (o scorretto) funzionamento, ma è assicurarsi che ogni singola funzionalità sia testata in modo che si possa essere sicuri che ogni riga di codice funzioni correttamente. Senza strumenti di supporto scrivere test in cui si è sicuri che ogni riga di codice sia stata toccata è molto complesso, soprattutto in applicazioni avanzate o framework molto grandi.

Per venire incontro alle esigenze di chi scrive test spesso i framework per lo Unit Testing implementano soluzioni di Code Coverage: ogni qual volta un test viene eseguito viene fatto un check a basso livello per controllare quali righe di codice sono state interessate dall'esecuzione. Successivamente viene stilato un report in cui vengono segnalate le righe eseguite e quelle non eseguite in modo che lo sviluppatore possa migliorare i propri test di volta in volta.

L'analisi della copertura del codice viene effettuata in PHP sfruttando un'estensione nativa molto potente: XDebug. Tra le altre cose, per chi non lo sapesse, XDebug funge anche da debugger e profiler per PHP. È quindi essenziale che questa libreria sia installata (potete farlo utilizzando il repository PECL) affinché possiate sfruttare questa funzionalità offerta dal framework.

Per utilizzare questa funzionalità basta eseguire il comando phpunit con un'opzione aggiuntiva che specifica dove salvare il report di esecuzione di una suite di test:

phpunit --report /path/alla/mia/directory/di/report MyTestCase

Con questo semplice comando otterremo una serie di file di report che specificano le righe di codice interessate dall'esecuzione di un determinato test. In rosso saranno evidenziate le righe non interessate, in verde quello eseguite. Basterà quindi adattare successivamente i propri test in modo che l'esecuzione di codice sia coperta al 100% al fine di assicurarsi di aver sotto mano dei test completi.

Gli oggetti Mock

Quando si scrivono dei test ci si può trovare in situazioni in cui i normali metodi di asserzione o l'organizzazione standard dei test non bastano. Un esempio potrebbe essere quello in cui ci si trova nella necessità di dover controllare che un determinato metodo di un oggetto sia richiamato con determinate condizioni (numero di volte, numero di parametri, valore dei parametri, ecc ...) durante l'esecuzione di un test su altri componenti che, internamente, utilizzano il nostro oggetto.

In PHPUnit questo problema si risolve sfruttando gli oggetti Mock. Un oggetto Mock non è altro che un oggetto che sostituisce uno realmente esistente in modo da emularne il comportamento. Utilizzando un oggetto Mock al posto di quello standard possiamo assicurarci che tutto avvenga per il verso giusto senza dover modificare il test o la funzionalità testata che utilizza indirettamente il nostro oggetto. Scrivere degli oggetti Mock a mano però è uno spreco di tempo inutile, soprattutto se gli oggetti da testare sono molti. PHPUnit ci viene incontro anche in questo caso fornendoci delle funzionalità che permettono in modo trasparente di generare oggetti Mock che sostituiscano quello originale in modo che ci si possa assicurare che l'esecuzione avvenga correttamente.

Vediamo un esempio tratto dalla documentazione ufficiale (a cui comunque rimando per approfondire l'argomento); in questo esempio il programmatore di trova di fronte al fatto di doversi assicurare che il metodo update di un oggetto Observer venga richiamato esattamente una volta con la stringa something come parametro. L'oggetto observer però è utilizzato internamente da un oggetto Subject, quindi non abbiamo modo di controllare questo comportamento se non attraverso un oggetto sostitutivo che controlli il comportamento.

<?php

require_once 'PHPUnit/Framework.php';

class ObserverTest extends PHPUnit_Framework_TestCase
{
        public function testUpdateIsCalledOnce()
        {
                $observer = $this->getMock( 'Observer', array( 'update' ) );
                $observer       ->expects( $this->once() )
                                        ->method( 'update' )
                                        ->with( $this->equalTo( 'something' ) );
                
                $subject = new Subject;
                $subject->attach( $observer );
                
                $subject->doSomething();
        }
}

?>

La prima operazione che svolgiamo è quella di creare un oggetto Mock; l'oggetto viene generato internamente dal framework attraverso il metodo getMock che accetta due parametri: come primo il nome della classe base che dovrà essere sostituita, come secondo un array di metodi sui cui vogliamo effettuare i controlli. In questo modo avremo un nuovo oggetto che si comporta come quello specificato (Observer in questo caso) ma ci permette operazioni aggiuntive di controllo sulla sua esecuzione. Per specificare i controlli viene utilizzata una metodologia chiamata Fluent Interface che, in parole povere, permette di ridurre il numero di oggetti intermedi utilizzati per la definizione di casi complessi. Nella nostra situazione ad esempio dobbiamo specificare che il metodo update sia chiamato una sola volta con il parametro something; per farlo utilizziamo una sequenza di chiamate:

  • il metodo expects accetta come parametro un oggetto che indica la condizione di chiamata dell'oggetto (nel nostro caso once, ma sono presenti molte altre condizioni che potete trovare nella documentazione ufficiale);
  • all'oggetto restituito dalla chiamata precedente possiamo specificare quale metodo vogliamo tenere sotto controllo;
  • ed infine indicare quali sono le condizioni che la chiamata deve soddisfare utilizzando il metodo with. Questo metodo accetta un numero di parametri variabile per ogni argomento che ci si aspetta venga passato al metodo. Ognuno di questi parametri è a sua volta un oggetto che controlla il valore accettato: nel nostro caso questo oggetto è generato utilizzando il metodo equalsTo e controlla che il parametro passato contenga effettivamente something.

Successivamente si procede con il codice standard che inizializza il Subject, passa l'observer di Mock ed esegue il metodo da testare. L'oggetto Subject si comporterà come è stato programmato, e quando eseguirà il metodo update dell'Observer il framework effettuerà tutti i controlli del caso e notificherà un fallimento in caso di problemi.

Il concetto che sta alla base degli oggetti Mock sono gli oggetti Matcher: questi oggetti sono eseguiti gerarchicamente dal framework per controllare che i metodi da testare siano chiamati correttamente in base a determinate esigenze scelte in fase di definizione dell'oggetto Mock. Come accennato precedentemente per evitare che si debbano creare molto oggetti intermedi, viene utilizzato il concetto di Fluent Interface che fornisce metodi da utilizzare in cascata per nascondere la generazione degli oggetti, che viene fatta internamente.

I matcher sono molti, e per avere una lista esaustiva di questi consiglio la lettura della guida ufficiale e delle API documentate.

Generazione automatica dei test

Prima di concludere ritengo interessante introdurre la generazione automatica dei test. PHPUnit fornisce la possibilità di generare automaticamente semplici test partendo dal codice sorgente delle proprie classi. L'unica necessità è quella di aggiungere della annotazioni al codice attraverso al documentazione dei metodi che si vuol testare. Queste annotazioni verranno interpretate da PHPUnit e serviranno da base per la generazione dei test.

<?php

/// ...

class Math
{
/**
        * @assert (0, 0) == 0
        * @assert (0, 1) == 1
        * @assert (1, 0) == 1
        * @assert (1, 1) == 2
        */
 public function sum( $a, $b )
        {
                return ( $a + $b );
        }
}

?>

Utilizzando il comando

phpunit --skeleton Math

genereremo in automatico un TestCase (chiamato MathTest.php nel nostro caso) che si occuperà di controllare la validità di tutte le asserzioni specificate nel commento. La struttura di un'annotazione è molto semplice: si utilizza la keyword assert a cui si passano come parametri i valori che si vuole vengano passati al metodo a cui è stato assegnato il commento. Poi viene specificato un operatore di paragone ed il valore contro il quale si vuole fare il controllo.

Conclusione

Siamo giunti alla fine della serie di articoli dedicata allo Unit Testing. Spero che alcuni di voi abbiamo iniziato a comprendere l'importanza di avere codice stabile e funzionante, testato in modo automatico grazie a tool specifici e ben organizzati. PHPUnit fornisce anche qualche funzionalità aggiuntiva oltre a quelle descritte, che potrebbe risultare molto utile in alcune situazioni (come l'integrazione con Selenium per il test automatici delle interfacce utente e le operazioni di Logging). Per eventuali approfondimenti consiglio caldamente la lettura della guida a PHPUnit, che potete trovare gratuitamente sul sito ufficiale del framework.


Ti consigliamo anche