Negli ultimi due giorni abbiamo rivisto tutte le feature imparate durante i primi cinque giorni del calendario dell'avvento per personalizzare le caratteristiche di Jobeet e per aggiungerne di nuove. In questo processo abbiamo anche accennato ad alcuni punti più avanzati di symfony.
Oggi inizieremo a parlare di qualcosa di completamente diverso: i test automatici. Siccome l'argomento è vasto, prenderà due giorni per poter coprire tutto.
I test in symfony
Ci sono due tipi diversi di test automatici in symfony: i test unitari e i test funzionali.
I test unitari verificano che ogni metodo e ogni funzione funzionino correttamente. Ogni test deve essere più indipendente possibile dagli altri.
D'altro canto, i test funzionali verificano che l'applicazione risultante si comporti correttamente nel suo insieme.
Tutti i test in symfony sono collocati sotto la cartella test/
del progetto.
Essa contiene due sotto-cartelle, una per i test unitari (test/unit/
) e una
per i test funzionali (test/functional/
).
I test unitari saranno coperti dal tutorial di oggi, mentre quello di domani sarà dedicato ai test funzionali.
I test unitari
Scrivere i test unitari è forse una delle best practice più difficili da mettere in atto nello sviluppo web. Siccome gli sviluppatori web non sono molto abituati a testare il proprio lavoro, si levano molte domande: devo scrivere i test prima di implementare una feature? Di cosa ho bisogno per un test? I miei test devono coprire ogni singolo caso limite? Come posso essere sicuro che tutto sia testato bene? Ma di solito la prima domanda è molto più semplice: da dove iniziare?
Anche se molto votato ai test, l'approccio di symfony è pragmatico: è sempre meglio avere alcuni test che nessuno. Avete già un sacco di codice e nessun test? Nessun problema. Non c'è bisogno di avere una suite completa di test per beneficiare del vantaggio di avere i test. Si può iniziare aggiungendo dei test ogni volta che si trova un bug nel codice. Nel tempo, il codice diventerà migliore, la copertura del codice salirà e diventerete più fiduciosi. Iniziando con un approccio pragmatico, vi sentirete più a vostro agio coi test nel tempo. Il prossimo passo è scrivere test per le nuove feature. In men che non si dica, diventerete dei fanatici dei test.
Il problema con molte librerie di test è la loro curva di apprendimento ripida. Per questo symfony fornisce una libreria di test molto semplice, lime, per rendere la scrittura dei test incredibilmente facile.
note
Anche se questo tutorial descrive approfonditamente la libreria lime, si può usare una qualsiasi libreria di test, come l'ottima PHPUnit.
Il framework di test lime
Tutti i test unitari scritti col framework lime iniziano con lo stesso codice:
require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1);
Innanzitutto, viene incluso il file iniziale unit.php
, per inizializzare alcune
cose. Poi, viene creato un oggetto lime_test
, a cui è passato come parametro
il numero di test pianificati.
note
Il piano consente a lime di mostrare un messaggio di errore nel caso in cui vengano eseguiti meno test di quanti pianificati (ad esempio se un test genera un errore fatale di PHP).
I test funzionano chiamando un metodo o una funzione con un insieme di input predefiniti e poi confrontando i risultati con l'output atteso. Questo confronto determina se un test passa o fallisce.
Per facilitare il confronto, l'oggetto lime_test
fornisce diversi metodi:
Metodo | Descrizione |
---|---|
ok($test) |
Testa un condizione e passa se è vera |
is($value1, $value2) |
Confronta due valori e passa se sono uguali (== ) |
isnt($value1, $value2) |
Confronta due valori e passa se sono diversi |
like($string, $regexp) |
Testa una stringa su un'espressione regolare |
unlike($string, $regexp) |
Verifica che una stringa non soddisfi un'espressione |
regolare | |
is_deeply($array1, $array2) |
Verifica che due array abbiano gli stessi valori |
tip
Potreste chiedervi perché lime definisca così tanti metodi di test, visto che
tutti i test potrebbero essere scritti usando il metodo ok()
. Il beneficio
dei metodi alternativi risiede nei messaggi di errore più espliciti in caso
di test fallito e nella leggibilità migliorata dei test.
L'oggetto lime_test
fornisce anche altri metodi di test utili:
Metodo | Descrizione |
---|---|
fail() |
Fallisce sempre--utile per testare le eccezioni |
pass() |
Passa sempre--utile per testare le eccezioni |
skip($msg, $nb_tests) |
Conta come $nb_tests test--utile per i test |
condizionali | |
todo() |
Conta come un test--utile per i test ancora da |
scrivere |
Infine, il metodo comment($msg)
mostra un commento ma non esegue test.
Eseguire i test unitari
Tutti i test unitari sono memorizzati nella cartella test/unit/
. Per convenzione,
i test hanno un nome che deriva dalla classe che testano e un suffisso Test
.
Sebbene sia possibile organizzare i test nella cartella test/unit/
in qualsiasi
modo, raccomandiamo di replicare la struttura della cartella lib/
.
Creiamo un file test/unit/JobeetTest.php
e inseriamoci il seguente codice:
// test/unit/JobeetTest.php require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1); $t->pass('This test always passes.');
Per lanciare i test, si può eseguire direttamente il file:
$ php test/unit/JobeetTest.php
Oppure usare il task test:unit
:
$ php symfony test:unit Jobeet
note
la linea di comando di Windows sfortunatamente non è in grado di
evidenziare i risultati dei test in rosso o in verde. Ma se si usa Cygwin,
è possibile forzare symfony a usare i colori passando l'opzione --color
al task.
Testare slugify
Iniziamo il nostro viaggio nel fantastico mondo dei test unitari scrivendo dei test
per il metodo Jobeet::slugify()
.
Abbiamo creato il metodo ~slug~ify()
nel giorno 5 per pulire una stringa, in modo
tale che possa essere inclusa in un URL in modo sicuro. La conversione consiste in
alcune trasformazioni di base, come convertire tutt i caratteri non-ASCII in un
trattino (-
), oppure convertire la stringa in minuscolo:
Input | Output |
---|---|
Sensio Labs | sensio-labs |
Paris, France | paris-france |
Sostituiamo il contenuto del file di test col seguente codice:
// test/unit/JobeetTest.php require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6); $t->is(Jobeet::slugify('Sensio'), 'sensio'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs'); $t->is(Jobeet::slugify('paris,france'), 'paris-france'); $t->is(Jobeet::slugify(' sensio'), 'sensio'); $t->is(Jobeet::slugify('sensio '), 'sensio');
Dando un'occhiata più da vicino ai test che abbiamo scritto, si può notare che ogni riga testa una sola cosa. Questo va tenuto a mente quando si scrivono i test unitari. Testare una sola cosa alla volta.
Ora si può eseguire il file del test. Se tutti i test passano, come ci aspettiamo, si potrà gustare la "barra verde". Se no, l'infame "barra rossa" avvertirà che alcuni test non sono passati e che sarà necessario sistemarli.
Se un test fallisce, l'output fornirà alcune informazioni sul perché; ma se si hanno centinaia di test in un file, può essere difficoltoso identificare rapidamente il comportamento che fallisce.
Tutti i metodi dei test accettano come ultimo parametro una stringa, che serve
da descrizione per il test. È molto utile, perché costringe a descrivere
cosa si sta veramente testando. Può anche servire come forma di documentazione
per il comportamento che ci si aspetta da un metodo. Aggiungiamo alcuni
messaggi al file dei test slugify
:
require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6); $t->comment('::slugify()'); $t->is(Jobeet::slugify('Sensio'), 'sensio', '::slugify() converts all characters to lower case'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces a white space by a -'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces several white spaces by a single -'); $t->is(Jobeet::slugify(' sensio'), 'sensio', '::slugify() removes - at the beginning of a string'); $t->is(Jobeet::slugify('sensio '), 'sensio', '::slugify() removes - at the end of a string'); $t->is(Jobeet::slugify('paris,france'), 'paris-france', '::slugify() replaces non-ASCII characters by a -');
La stringa di descrizione del test è anche uno strumento prezioso quando si prova a immaginare cosa testare. Si può vedere uno schema nelle stringhe dei test: sono frasi che descrivono come il metodo si deve comportare e iniziano sempre col nome del metodo da testare.
Aggiungere test per nuove feature
Lo slug per una stringa vuota è una stringa vuota. Lo si può testare,
funzionerà. Ma una stringa vuota in un URL non è una buona idea. Cambiamo
il metodo slugify()
in modo che restituisca la stringa "n-a" in caso
di stringa vuota.
Si possono scrivere i test prima, poi aggiornare il metodo, o viceversa. È solo una questione di gusti, ma scrivere il test prima dà la sicurezza che il codice implementi veramente quello che si è pianificato:
$t->is(Jobeet::slugify(''), 'n-a', '::slugify() converts the empty string to n-a');
Questa metodologia di sviluppo, in cui si scrivono prima i test e poi si implementano le funzionalità, è nota come Test Driven Development (TDD).
Se ora si lanciano i test, si dovrebbe avere una barra rossa, Se no, vuol dire che la feature è già implementata o che i test non testano quello che dovrebbero testare.
Ora, modifichiamo la classe Jobeet
e aggiungiamo la condizione seguente
all'inizio:
// lib/Jobeet.class.php static public function slugify($text) { if (empty($text)) { return 'n-a'; } // ... }
Il test ora dovrebbe passare, come ci si aspettava, e possiamo goderci la barra verde, ma solo se ci siamo ricordati di aggiornare il piano dei test. Altrimenti, si avrà un messaggio che dice che si sono pianificati sei test, ma se ne è eseguito uno in più. Tenere aggiornato il conteggio dei test è importante, perché ci tiene informati se lo script dei test è morto prematuramente.
Aggiungere test a causa di un bug
Ipotizziamo che sia passato del tempo e che un utente abbia segnalato uno strano bug: alcuni link di lavori puntano a una pagina di errore 404. Dopo alcune investigazioni, si scopre che per qualche ragione questi lavori mancano dello slug della compagnia, della posizione o del posto.
Come è possibile?
Avete cercato in tutte le righe del database e le colonne sono sicuramente
non vuote. Ci pensate per un po' e poi trovate la soluzione. Quando una stringa
contiene solo caratteri non-ASCII, il metodo slugify()
la converte in una
stringa vuota. Contenti per aver trovato la causa, aprite la classe Jobeet
e risolvete il problema nel modo giusto. È una cattiva idea.
Prima, aggiungiamo un test:
$t->is(Jobeet::slugify(' - '), 'n-a', '::slugify() converts a string that only contains non-ASCII characters to n-a');
Dopo aver controllato che il test non passa, modifichiamo la classe Jobeet
e spostiamo il controllo della stringa vuota alla fine del metodo:
static public function slugify($text) { // ... if (empty($text)) { return 'n-a'; } return $text; }
Il nuovo test ora passa, come tutti gli altri. slugify()
aveva un bug,
nonostante la copertura del 100%.
Non si può pensare a tutti i casi limite quando si scrivono i test, e va bene. Ma quando se ne scopre uno, bisogna scrivere un test prima di sistemare il codice. Vuol dire anche che il codice diventerà migliore nel tempo, che è sempre una buona cosa.
Test unitari con Propel
Configurazione del database
I test unitari con le classi del modello di Propel sono un poco più complessi, poiché richiedono una connessione al database. Ne abbiamo già una, che usiamo per lo sviluppo, ma è una buona abitudine creare un database dedicato per i test.
Durante il giorno 1, abbiamo introdotto gli ambienti come un modo per
variare le impostazioni di un'applicazione. Per default, tutti i
test di symfony sono eseguiti nell'ambiente test
, quindi configuriamo
un database differente per l'ambiente test
:
$ php symfony configure:database --env=test "mysql:host=localhost;dbname=jobeet_test" root mYsEcret
L'opzione env
dice al task che la configurazione del database è solo per
l'ambiente dev
. Quando abbiamo usato questo task durante il giorno 3, non
abbiamo passato nessuna opzione env
, quindi la configurazione è stata
applicata a tutti gli ambienti.
note
Se siete curiosi, aprite il file di configurazione config/~databases.yml~
per
vedere come symfony rende semplice modificare la configurazione a seconda
dell'ambiente.
Ora che abbiamo configurato il database, possiamo inizializzarlo usando il task
propel:insert-sql
task:
$ mysqladmin -uroot -pmYsEcret create jobeet_test $ php symfony propel:insert-sql --env=test
Dati dei test
Ora che abbiamo un database dedicato per i nostri test, ci serve un modo
per caricare alcuni dati per i test. Durante il giorno 3, avete imparato
a usare il task propel:data-load
, ma per i test occorre ricaricare i dati
ogni volta che sono eseguiti, per porre il database in uno stato conosciuto.
Il task propel:data-load
usa internamente la classe
sfPropelData
per caricare i dati:
$loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
note
L'oggetto sfConfig
può essere usato per ottenere il percorso completo di
una sotto-cartella del progetto. Usandolo, si può personalizzare la struttura
predefinita delle cartelle.
Il metodo loadData()
accetta come primo parametro una cartella o un file.
Può accettare anche un array di cartelle e/o di file.
Abbiamo già creato alcuni dati iniziali nella cartella data/fixtures/
.
Per i test, inseriremo le fixture nella cartella test/fixtures/
. Queste
fixture saranno usate per i test unitari e funzionali di Propel.
Per ora, copiamo i file dalla cartella data/fixtures/
alla cartella
test/fixtures/
.
Testare JobeetJob
Creiamo alcuni test unitari per la classe del modello JobeetJob
.
Siccome tutti i nostri test unitari di Propel inizieranno con lo stesso
codice, creiamo un file Propel.php
nella cartella bootstrap/
dei
test, con il seguente codice:
// test/bootstrap/Propel.php include(dirname(__FILE__).'/unit.php'); $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true); new sfDatabaseManager($configuration); $loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
Lo script è praticamente auto-esplicante:
Come per i front controller, inizializziamo un oggetto configurazione per l'ambiente
test
:$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true);
Creiamo un gestore di database. Esso inizializza la connessione Propel caricando il file di configurazione
databases.yml
.new sfDatabaseManager($configuration);
Carichiamo i nostri dati di test usando
sfPropelData
:$loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');
note
Propel si connette al database solo se ha delle istruzioni SQL da eseguire.
Ora che è tutto a posto, possiamo iniziare a testare la classe JobeetJob
.
Innanzitutto, dobbiamo creare il file JobeetJobTest.php
in test/unit/model
:
// test/unit/model/JobeetJobTest.php include(dirname(__FILE__).'/../../bootstrap/Propel.php'); $t = new lime_test(1);
Poi, iniziamo aggiungendo un testo per il metodo getCompanySlug()
:
$t->comment('->getCompanySlug()'); $job = JobeetJobPeer::doSelectOne(new Criteria()); $t->is($job->getCompanySlug(), Jobeet::slugify($job->getCompany()), '->getCompanySlug() return the slug for the company');
Notare che testiamo solo il metodo getCompanySlug()
e non se lo slug sia
corretto o meno, perché l'abbiamo già testato altrove.
Scrivere test per il metodo save()
è un po' più complesso:
$t->comment('->save()'); $job = create_job(); $job->save(); $expiresAt = date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days')); $t->is($job->getExpiresAt('Y-m-d'), $expiresAt, '->save() updates expires_at if not set'); $job = create_job(array('expires_at' => '2008-08-08')); $job->save(); $t->is($job->getExpiresAt('Y-m-d'), '2008-08-08', '->save() does not update expires_at if set'); function create_job($defaults = array()) { static $category = null; if (is_null($category)) { $category = JobeetCategoryPeer::doSelectOne(new Criteria()); } $job = new JobeetJob(); $job->fromArray(array_merge(array( 'category_id' => $category->getId(), 'company' => 'Sensio Labs', 'position' => 'Senior Tester', 'location' => 'Paris, France', 'description' => 'Testing is fun', 'how_to_apply' => 'Send e-Mail', 'email' => 'job@example.com', 'token' => rand(1111, 9999), 'is_activated' => true, ), $defaults), BasePeer::TYPE_FIELDNAME); return $job; }
note
Ogni volta che si aggiungono test, non dimenticare di aggiornare il numero
di test pianificati (il piano) nel metodo costruttore lime_test
. Per il file
JobeetJobTest
, dobbiamo cambiarlo da 1
a 3
.
Testare le altre classi Propel
Possiamo ora aggiungere test per tutte le altre classi Propel. Essendovi abituati al processo di scrittura dei test unitari, dovrebbe essere piuttosto semplice.
Insieme di test unitari
Il task test:unit
può anche essere usato per lanciare tutti i test
unitari per un progetto:
$ php symfony test:unit
Il task mostra per ogni file se i test passano o falliscono:
tip
Se il task test:unit
restituisce uno stato "dubious" per un file, vuol dire
che lo script è terminato prima della fine. Eseguire il test di quel file da
solo fornirà il messaggio di errore specifico.
A domani
Anche se testare un'applicazione è molto importante, sappiamo che alcuni di voi sono stati tentati di saltare il tutorial di oggi. Siamo contenti che non l'abbiate fatto.
Certo, abbracciare symfony implica imparare tutte le grandi feature che un framework fornisce, ma anche la sua filosofia di sviluppo e le best practice che predica. E i test sono una di queste. Prima o poi, i test unitari vi salveranno la giornata. Danno una solida sicurezza sul proprio codice e la libertà di rifattorizzare senza paura. I test unitari sono un guardiano sicuro che avverte quando qualcosa si spezza. Lo stesso framework symfony ha oltre 9000 test.
Domani scriveremo alcuni test funzionali per i moduli job
e category
.
Fino ad allora, prendetevi un po' di tempo per scrivere altri test
unitari per le classi del modello di Jobeet.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.