Development

Documentation/it_IT/book/1.1/15-Unit-and-Functional-Testing

You must first sign up to be able to contribute.

Capitolo 15 - Test unitari e funzionali

I test automatici sono la più grande innovazione nella programmazione dall'orientamento agli oggetti. Particolarmente incidenti nello sviluppo di applicazioni web, essi possono garantirne la qualità anche se i rilasci sono numerosi. Symfony mette a disposizione una varietà di strumenti per facilitare l'automazione dei test, e questo capitolo li introduce.

Test automatici

Ogni sviluppatore con esperienza nello sviluppo di applicazioni web conosce il tempo necessario ad un buon testing. Scrivere i test case, farli girare ed analizzare i risultati è un lavoro noioso. Inoltre i requisiti delle applicazioni web cambiano continuamente, e ciò porta ad un continuo rilascio di nuove versioni ed al bisogno di refactoring del codice. In tale contesto la nascita di nuovi errori è molto probabile.

Ecco perchè i test automatici sono suggeriti, se non addirittura necessari, come parte di un ambiente di sviluppo di successo. Un insieme di test case può garantire che un'applicazione faccia veramente ciò per cui è stata pensata. Anche se il codice viene modificato di continuo, i test automatici prevengono regressioni accidentali. Inoltre, essi costringono lo sviluppatore a scrivere i test in un formato standard, rigido, che risulta comprensibile ai framework di testing.

I test automatici talvolta possono sostituire la documentazione dello sviluppatore, in quanto essi spiegano chiaramente ciò che l'applicazione dovrebbe fare. Un buon insieme di test mostra quale output ci si dovrebbe aspettare per un dato set di input, il che è un buon metodo per spiegare le finalità di un metodo.

Il framework symfony applica tale principio a se stesso. Il codice interno di symfony è validato tramite test automatici. Tali unit e functional test non sono inclusi nel pacchetto PEAR, ma li puoi scaricare tramite SVN o navigare all'indirizzo [http://www.symfony-project.com/trac/browser/branches/1.1/test].

Test unitari e test funzionali

I test unitari confermano che un "pezzo" di codice (componente) unitario fornisce un determinato output in funzione di un dato input. Essi convalidano il funzionamento di funzioni e metodi in ogni singolo caso. Le unit test gestiscono un caso alla volta, per cui ad esempio un singolo metodo potrebbe aver bisogno di diverse unit test se funziona diversamente in certe situazioni.

I test unitari non convalidano una semplice conversione input-to-output, ma una funzionalità completa. Ad esempio, un sistema di cache può essere convalidato esclusivamente tramite test funzionali, perché esso coinvolge diversi step: la prima volta che una pagina viene richiesta essa viene renderizzata; la seconda volta essa viene recuperata dalla cache. Per cui i test funzionali convalidano un processo e richiedono uno scenario. In symfony, dovresti scrivere test funzionali per tutte le tue azioni.

Per le interazioni più complesse, questi due tipi potrebbero non bastare. Ad esempio le interazioni Ajax richiedono un browser che esegua JavaScript, per cui per testarle automaticamente c'è bisogno di uno strumento speciale di terze parti. Inoltre gli effetti visuali possono essere convalidati solo da umani.

Se hai un approccio estensivo ai test automatici, probabilmente avrai bisogno di usare una combinazione di tutti questi metodi. Come linee guida, ricorda di mantenere i test semplici e leggibili.

NOTE I test automatici lavorano confrontando un risultato con un output previsto. In altre parole, essi valutano asserzioni (espressioni tipo $a == 2). Il valore di un'asserzione può essere true o false, e determina quando il test abbia successo o fallisca. La parola "asserzione" è usata comunemente quando si parla di tecniche di test automatici.

`

Sviluppo test-driven

Nella metodologia test-driven development (TDD), i test vengono scritti prima del codice. Scrivere un test ti aiuta a focalizzarti su ciò che la funzione deve fare effettivamente prima di svilupparla. È una buona pratica raccomandata anche da altre metodologie, come Extreme Programming (XP). Inoltre questa tecnica tiene conto dell'innegabile fatto che se non scrivi i test prima non li scriverai mai.

Ad esempio, immagina di dover sviluppare una funzione di text-stripping. La funzione rimuove gli spazi bianchi all'inizio ed alla fine della stringa, sostituisce tutti i caratteri non alfabetici con un trattino basso e trasforma tutti i caratteri maiuscoli in minuscoli. In TDD, prima di tutto penseresti a tutti i casi possibili e faresti un esempio di input con l'output previsto per ogni possibile caso, come mostrato in Tabella 15-1.

Tabella 15-1 - Una lista di casi di test per una funzione di text-stripping.

Input Expected Output
`" foo "` `"foo"`
`"foo bar"` `"foo_bar"`
`"-)foo:..=bar?"` `"__foo____bar_"`
`"FooBar"` `"foobar`"
`"Don't foo-bar me!"` `"don_t_foo_bar_me_"`

Scriveresti i test unitari, li faresti girare e vedresti che falliscono. Poi aggiungeresti il codice necessario a gestire il primo caso, le faresti girare di nuovo, vedresti che il primo passa con successo ed andresti avanti così. Alla fine, quando tutti i test passano con successo, la funzione è corretta. Un'applicazione scritta con una metodologia test-driven finisce generalmente per avere tanto codice quanto ne è necessario per i test. Dato che non vuoi passare tempo a debuggare i casi di test, mantienili semplici.

NOTE Rifattorizzare un metodo potrebbe far sorgere bug non presenti in precedenza. Ecco perché è buona pratica far girare tutti i test automatici prima del rilascio di un'applicazione in produzione; ciò è chiamato regression testing.

Il framework di testing Lime

Ci sono diversi framework di testing nel mondo di PHP, di cui i più famosi sono senz'altro PhpUnit e SimpleTest. Symfony possiede il proprio, chiamato lime. È basato sulla libreria Perl Test::More ed è TAP compliant, che significa che il risultato dei test viene visualizzato secondo le specifiche del Test Anything Protocol, pensato per una migliore leggibilità dell'ouput dei test.

Lime fornisce supporto per i test unitari. È più leggero degli altri framework di testing di PHP ed ha diversi vantaggi: * Lancia i file di test in una sandbox per evitare strani effetti collaterali tra un test e l'altro. Non tutti i framework di testing garantiscono un ambiente pulito per ogni test. * I test di lime sono molto leggibili, e così anche il loro output. Sui sistemi compatibili, lime usa colori diversi per sottolineare dati importanti. * Symfony stesso usa i test di lime per il regression testing, per cui puoi trovare diversi esempi di test unitari e funzionali nel suo codice sorgente. * Il core di lime è convalidato da test unitari. * È scritto in PHP, ed è veloce e scritto bene. È contenuto in un singolo file, lime.php, senza alcuna dipendenza.

I test descritti in seguito si basano sulla sintassi di lime, e funzionano con qualsiasi installazione di symfony.

NOTE I test unitari e funzionali non sono pensati per essere lanciati in produzione. Si tratta di strumenti di sviluppo, e come tali dovrebbero essere usati sulla macchina dello sviluppatore e non sul server di produzione.

Test unitari

I test unitari di symfony sono semplici file PHP che finiscono con Test.php e si trovano nella cartella test/unit/ della tua applicazione. Essi seguono una semplice e leggibile sintassi.

Come sono fatti?

Il Listato 15-1 mostra un tipico insieme di test unitari per la funzione strtolower(). Comincia con l'istanza dell'oggetto lime_test (non ti preoccupare dei parametri per ora). Ogni test unitario è una chiamata ad un metodo di tale istanza. L'ultimo parametro di tali metodi è sempre una stringa opzionale che serve come output.

Listato 15-1 - Esempio di unit test, in test/unit/strtolowerTest.php

<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
require_once(dirname(__FILE__).'/../../lib/strtolower.php');

$t = new lime_test(7, new lime_output_color());

// strtolower()
$t->diag('strtolower()');
$t->isa_ok(strtolower('Foo'), 'string',
    'strtolower() returns a string');
$t->is(strtolower('FOO'), 'foo',
    'strtolower() transforms the input to lowercase');
$t->is(strtolower('foo'), 'foo',
    'strtolower() leaves lowercase characters unchanged');
$t->is(strtolower('12#?@~'), '12#?@~',
    'strtolower() leaves non alphabetical characters unchanged');
$t->is(strtolower('FOO BAR'), 'foo bar',
    'strtolower() leaves blanks alone');
$t->is(strtolower('FoO bAr'), 'foo bar',
    'strtolower() deals with mixed case input');
$t->is(strtolower(''), 'foo',
    'strtolower() transforms empty strings into foo');

Lancia questo insieme di test dalla linea di comando con il task test:unit. L'output della linea di comando è molto esplicito, e ti aiuta a capire quale test è passato e quale ha fallito. Vedi l'output dell'esempio nel Listato 15-2.

Listato 15-2 - Lanciare un singolo test unitario da linea di comando

php symfony test:unit strtolower

1..7
# strtolower()
ok 1 - strtolower() returns a string
ok 2 - strtolower() transforms the input to lowercase
ok 3 - strtolower() leaves lowercase characters unchanged
ok 4 - strtolower() leaves non alphabetical characters unchanged
ok 5 - strtolower() leaves blanks alone
ok 6 - strtolower() deals with mixed case input
not ok 7 - strtolower() transforms empty strings into foo
#     Failed test (.\batch\test.php at line 21)
#            got: ''
#       expected: 'foo'
# Looks like you failed 1 tests of 7.

TIP L'istruzione include all'inizio del Listato 15-1 è opzionale, ma rende il file uno script PHP indipendente che puoi far girare senza l'ausilio della linea di comando, chiamando test/unit/strtolowerTest.php.

Metodi dei test unitari

L'oggetto lime_test è dotato di una grande varietà di metodi di test, come mostrato dalla Tabella 15-2.

Tabella 15-2 - Metodi dell'oggetto lime_test per i test unitari

Metodo Descrizione
`diag($msg)` Outputs a comment but runs no test
`ok($test, $msg)` Tests a condition and passes if it is true
`is($value1, $value2, $msg)` Compares two values and passes if they are equal (`==`)
`isnt($value1, $value2, $msg)` Compares two values and passes if they are not equal
`like($string, $regexp, $msg)` Tests a string against a regular expression
`unlike($string, $regexp, $msg)` Checks that a string doesn't match a regular expression
`cmp_ok($value1, $operator, $value2, $msg)` Compares two arguments with an operator
`isa_ok($variable, $type, $msg)` Checks the type of an argument
`isa_ok($object, $class, $msg)` Checks the class of an object
`can_ok($object, $method, $msg)` Checks the availability of a method for an object or a class
`is_deeply($array1, $array2, $msg)` Checks that two arrays have the same values
`include_ok($file, $msg)` Validates that a file exists and that it is properly included
`fail()` Always fails--useful for testing exceptions
`pass()` Always passes--useful for testing exceptions
`skip($msg, $nb_tests)` Counts as `$nb_tests` tests--useful for conditional tests
`todo()` Counts as a test--useful for tests yet to be written

La sintassi è abbastanza diretta; nota che la maggior parte dei metodi accetta un messaggio come terzo parametro. Tale messaggio viene visualizzato nell'output quando il test passa con successo. In effetti, il metodo migliore per capire questi metodi è quello di usarli, per cui dai un'occhiata al Listato 15-3, che li usa tutti.

Listato 15-3 - Metodi di test dell'oggetto lime_test, in test/unit/exampleTest.php

<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');

// Stub objects and functions for test purposes
class myObject
{
  public function myMethod()
  {
  }
}

function throw_an_exception()
{
  throw new Exception('exception thrown');
}

// Initialize the test object
$t = new lime_test(16, new lime_output_color());

$t->diag('hello world');
$t->ok(1 == '1', 'the equal operator ignores type');
$t->is(1, '1', 'a string is converted to a number for comparison');
$t->isnt(0, 1, 'zero and one are not equal');
$t->like('test01', '/test\d+/', 'test01 follows the test numbering pattern');
$t->unlike('tests01', '/test\d+/', 'tests01 does not follow the pattern');
$t->cmp_ok(1, '<', 2, 'one is inferior to two');
$t->cmp_ok(1, '!==', true, 'one and true are not identical');
$t->isa_ok('foobar', 'string', '\'foobar\' is a string');
$t->isa_ok(new myObject(), 'myObject', 'new creates object of the right class');
$t->can_ok(new myObject(), 'myMethod', 'objects of class myObject do have amyMethod method');
$array1 = array(1, 2, array(1 => 'foo', 'a' => '4'));
$t->is_deeply($array1, array(1, 2, array(1 => 'foo', 'a' => '4')),
    'the first and the second array are the same');
$t->include_ok('./fooBar.php', 'the fooBar.php file was properly included');

try
{
  throw_an_exception();
  $t->fail('no code should be executed after throwing an exception');
}
catch (Exception $e)
{
  $t->pass('exception catched successfully');
}

if (!isset($foobar))
{
  $t->skip('skipping one test to keep the test count exact in the condition', 1);
}
else
{
  $t->ok($foobar, 'foobar');
}

$t->todo('one test left to do');

Troverai molti altri esempi dell'utilizzo di questi metodi nelle unit test di symfony.

TIP Ti potresti chiedere perchè usare is() al posto di ok(). Il messaggio di errore prodotto da is() è molto più esplicito; mostra entrambi i membri del test, mentre ok() dice semplicemente che la condizione fallisce.

Parametri di test

L'inizializzazione dell'oggetto lime_test prende come primo parametro il numero di test che devono essere eseguiti. Se il numero di test eseguiti alla fine è diverso da questo numero, l'output di lime di avvisa. Ad esempio, l'insieme di test del Listato 15-3 genera l'output del Listato 15-4. L'inizializzazione stipulava che dovessero girare 16 test, ma in effetti ne sono girati solo 15, per cui questo fatto viene indicato nell'output.

Listato 15-4 - La quantità dei test da eseguire ti aiuta nella pianificazione

php symfony test:unit example
1..16
# hello world
ok 1 - the equal operator ignores type
ok 2 - a string is converted to a number for comparison
ok 3 - zero and one are not equal
ok 4 - test01 follows the test numbering pattern
ok 5 - tests01 does not follow the pattern
ok 6 - one is inferior to two
ok 7 - one and true are not identical
ok 8 - 'foobar' is a string
ok 9 - new creates object of the right class
ok 10 - objects of class myObject do have a myMethod method
ok 11 - the first and the second array are the same
not ok 12 - the fooBar.php file was properly included
#     Failed test (.\test\unit\testTest.php at line 27)
#       Tried to include './fooBar.php'
ok 13 - exception catched successfully
ok 14 # SKIP skipping one test to keep the test count exact in the condition
ok 15 # TODO one test left to do
# Looks like you planned 16 tests but only ran 15.
# Looks like you failed 1 tests of 16.

Il metodo diag() non conta come test. Usalo per mostrare commenti, in modo che l'output dei tuoi test sia ordinato e leggibile. D'altra parte, i metodi todo() e skip() contano come test effettivi. Una combinazione pass()/fail() all'interno di un blocco try/catch conta come test singolo.

Una strategia di test pianificata per bene deve contenere un previsto numero di test. Troverai molto utile convalidare i tuoi file di test, specialmente in casi complicati dove i test girano all'interno di condizioni od eccezioni. E se il test fallisce in qualche punto, te ne accorgerai subito in quanto il numero finale di test non corrisponde a quello specificato durante l'inizializzazione.

Il secondo parametro del costruttore è un oggetto di output che estende la classe lime_output. La maggior parte delle volte, dato che i test sono pensati per girare in una CLI, l'output è un oggetto lime_output_color, che si avvantaggia dei colori della bash quando disponibili.

Il task test:unit

Il task test:unit, che serve a lanciare i test unitari da linea di comando, si aspetta una lista nomi di test oppure un file pattern. Vedi il Listato 15-5 per dettagli.

Listato 15-5 - Lanciare unit test

// Struttura della cartella Test
test/
  unit/
    myFunctionTest.php
    mySecondFunctionTest.php
    foo/
      barTest.php

> symfony test:unit myFunction                   ## Lancia myFunctionTest.php
> symfony test:unit myFunction mySecondFunction  ## Lancia both tests
> symfony test:unit 'foo/*'                      ## Lancia barTest.php
> symfony test:unit '*'                          ## Lancia all tests (recursive)

Stub, fixture e autoloading

In un test unitario, la funzionalità di autoloading non è attiva per default. Ogni classe da utilizzare deve essere definita all'interno del test stesso oppure essere inclusa come dipendenza esterna. Ecco perché molto spesso i file di test cominciano con un gruppo di inclusione, come mostrato nel Listato 15-6.

Listato 15-6 - Includere classi nei test

<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');

$t = new lime_test(7, new lime_output_color());

// isPathAbsolute()
$t->diag('isPathAbsolute()');
$t->is(sfToolkit::isPathAbsolute('/test'), true,
    'isPathAbsolute() returns true if path is absolute');
$t->is(sfToolkit::isPathAbsolute('\\test'), true,
    'isPathAbsolute() returns true if path is absolute');
$t->is(sfToolkit::isPathAbsolute('C:\\test'), true,
    'isPathAbsolute() returns true if path is absolute');
$t->is(sfToolkit::isPathAbsolute('d:/test'), true,
    'isPathAbsolute() returns true if path is absolute');
$t->is(sfToolkit::isPathAbsolute('test'), false,
    'isPathAbsolute() returns false if path is relative');
$t->is(sfToolkit::isPathAbsolute('../test'), false,
    'isPathAbsolute() returns false if path is relative');
$t->is(sfToolkit::isPathAbsolute('..\\test'), false,
    'isPathAbsolute() returns false if path is relative');

Nei test unitari, non devi istanziare solo l'oggetto che stai testando, ma anche quello da cui dipende. Dato che le unit test devono rimanere unitarie, la dipendenza da altre classi potrebbe far fallire più di un test quando la dipendenza viene interrotta. Inoltre, impostare oggetti reali potrebbe divenire costoso, sia in termini di codice che di tempo di esecuzione. Tieni presente che la velocità è cruciale, in quanto gli sviluppatori si stancano presto di un processo lento.

Ogni qualvolta cominci ad includere diversi script nei tuoi test, potresti sentire il bisogno dell'autoloading. A tale scopo, la classe sfCore (che deve essere inclusa manualmente) fornisce il metodo initSimpleAutoload(), che si aspetta un percorso assoluto come parametro. Tutte le classi in tale path saranno auto-caricate. Ad esempio, se tu volessi l'autoloading di tutte le classi nel path $sf_symfony_lib_dir/util/, comincia la tua unit test come segue:

[php]
require_once($sf_symfony_lib_dir.'/autoload/sfSimpleAutoload.class.php');
$autoload = sfSimpleAutoload::getInstance();
$autoload->addDirectory($sf_symfony_lib_dir.'/util');
$autoload->register();

TIP Gli oggetti Propel generati si basano su una lunga cascata di classi, per cui se tu volessi testarne uno l'autoloading diventa necessario. Nota che per far funzionare Propel, devi anche includere i file della cartella vendor/propel/ (per cui la chiamata a sfCore diventa sfCore::initSimpleAutoload(array(SF_ROOT_ DIR.'/lib/model', $sf_symfony_lib_dir.'/vendor/propel'));) ed aggiungere il core di Propel al percorso (chiamando set_include_path($sf_symfony_lib_dir.'/vendor'.PATH_SEPARATOR.SF_ROOT_DIR.PATH_SEPARATOR.get_include_path())).

Un altro buon meotodo per l'autoloading è l'uso di stub. Uno stub è un'implementazione alternativa di una classe dove i metodi reali vengono sostituiti con semplici dati preconfezionati. Esso imita il comportamento della classe reale, ma senza pagarne il costo. Un buon esempio di stub è una connessione a database od un'interfaccia ad un web service. Nel Listato 15-7, i test unitari per un'API di basano sulla classe WebService. Invece di chiamare il vero metodo fetch() della classe effettiva, il test utilizza uno stub che restituisce dati di esempio.

Listato 15-7 - Utilizzo di stub nelle unit test

[php]
require_once(dirname(__FILE__).'/../../lib/WebService.class.php');
require_once(dirname(__FILE__).'/../../lib/MapAPI.class.php');

class testWebService extends WebService
{
  public static function fetch()
  {
    return file_get_contents(dirname(__FILE__).'/fixtures/data/fake_web_service.xml');
  }
}

$myMap = new MapAPI();

$t = new lime_test(1, new lime_output_color());

$t->is($myMap->getMapSize(testWebService::fetch(), 100));

I dati di test possono essere più complicati di una stringa o di una chiamata ad un metodo. Complessi dati di test vengono spesso chiamati fixture. Per chiarezza di codice, è spesso molto meglio tenere le fixture in file separati, specialmente quando devono venire utilizzati da diverse unit test. Inoltre non dimenticare che symfony può trasformare facilmente un file YAML in un array tramite il metodo sfYAML::load(). Ciò significa che invece di scrivere lunghi array in PHP, puoi scrivere i tuoi dati di test in un file YAML, come mostrato nel Listato 15-8.

Listato 15-8 - Utilizzo di fixture nei test unitari

// In fixtures.yml:
-
  input:   '/test'
  output:  true
  comment: isPathAbsolute() returns true if path is absolute
-
  input:   '\\test'
  output:  true
  comment: isPathAbsolute() returns true if path is absolute
-
  input:   'C:\\test'
  output:  true
  comment: isPathAbsolute() returns true if path is absolute
-
  input:   'd:/test'
  output:  true
  comment: isPathAbsolute() returns true if path is absolute
-
  input:   'test'
  output:  false
  comment: isPathAbsolute() returns false if path is relative
-
  input:   '../test'
  output:  false
  comment: isPathAbsolute() returns false if path is relative
-
  input:   '..\\test'
  output:  false
  comment: isPathAbsolute() returns false if path is relative

// In testTest.php
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');
require_once($sf_symfony_lib_dir.'/yaml/sfYaml.class.php');

$testCases = sfYaml::load(dirname(__FILE__).'/fixtures.yml');

$t = new lime_test(count($testCases), new lime_output_color());

// isPathAbsolute()
$t->diag('isPathAbsolute()');
foreach ($testCases as $case)
{
  $t->is(sfToolkit::isPathAbsolute($case['input']), $case['output'],$case['comment']);
}

Testare le classi Propel

Testare le classi Propel è un po' più complesso, perché gli oggetti Propel generati si basano su una lunga serie di classi. Inoltre, devi fornire una connessione a database valida a Propel e devi anche inserire alcuni dati di test nel database.

Fortunatamente, è abbastanza facile perché symfony ti fornisce tutto ciò di cui hai bisogno:

  • Per l'autoloading, hai bisogno di inizializzare un oggetto configurazione
  • Per ottenere una connessione a database, hai bisogno di inizializzare la classe sfDatabaseManager
  • Per caricare dei dati di test, puoi usare la classe sfPropelData

Un tipo file di test di Propel è mostrato nel Listato 15-9.

Listato 15-9 - Classi di test per Propel

<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');

new sfDatabaseManager(ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true));
$loader = new sfPropelData();
$loader->loadData(sfConfig::get('sf_data_dir').'/fixtures');

$t = new lime_test(1, new lime_output_color());

// begin testing your model class
$t->diag('->retrieveByUsername()');
$user = UserPeer::retrieveByUsername('fabien');
$t->is($user->getLastName(), 'Potencier', '->retrieveByUsername() returns the User for the given username');

Test funzionali

I test funzionali convalidano parti delle tue applicazioni. Essi simulano una sessione di browsing, eseguono richieste, controllano gli elementi delle risposte, proprio come tu faresti manualmente per controllare che un'azione faccia correttamente ciò per cui è pensata. Nei test funzionali, fai girare uno scenario corrispondente ad un caso d'uso.

Come sono fatti i test funzionali?

Potresti far girare i tuoi test funzionali con un browser di testo e una grande quantità di asserzioni di espressioni regolari, ma ciò risulterebbe in una immensa perdita di tempo. Symfony mette a disposizione un oggetto speciale, chiamato sfBrowser, che si comporta come un browser collegato ad un'applicazione symfony senza avere realmente il bisogno di un server, e senza il rallentamento dovuto al protocollo HTTP. Esso fornisce accesso agli oggetti di core per ogni richiesta (richiesta, sessione, contesto, risposta). Symfony mette a disposizione inoltre un'estensione di tale classe chiamata sfTestBrowser, pensata esattamente per i test funzionali, che include tutte le capacità di sfBrowser con l'aggiunta di qualche metodo intelligente.

Un test funzionale comincia tradizionalmente con l'inizializzazione di un oggetto test browser. Tale oggetto esegue una richiesta ad un'azione e controlla la presenza di alcuni elementi nella risposta.

Ad esempio, ogni volta che generi lo scheletro di un modulo tramite i task generate:module o propel:generate-crud, symfony crea un semplice test funzionale per tale modulo. Il test fa una richiesta all'azione di default del modulo e controlla il codice di stato della risposta, modulo ed azioni calcolati dal sistema di routing, e la presenza di una certa frase nel contenuto della risposta. Per un modulo foobar, il file generato foobarActionsTest.php è fatto come quello del Listato 15-9.

Listato 15-9 - Test funzionale di default per un nuovo modulo, in tests/functional/frontend/foobarActionsTest.php

<?php

include(dirname(__FILE__).'/../../bootstrap/functional.php');

// Create a new test browser
$browser = new sfTestBrowser();

$browser->
  get('/foobar/index')->
  isStatusCode(200)->
  isRequestParameter('module', 'foobar')->
  isRequestParameter('action', 'index')->
  checkResponseElement('body', '!/This is a temporary page/')
;

TIP I metodi del browser restituiscono un oggetto sfTestBrowser, per cui puoi concatenare le chiamate dei tuoi file di test per una migliore leggibilità. Ciò viene chiamato interfaccia fluida all'oggetto, in quanto niente ferma le chiamate al metodo.

Un test funzionale può contenere diverse richieste e asserzioni più complesse; ne scoprirai tutte le possibilità nelle sezioni a venire.

Per lanciare un test funzionale, usa il task test:functional della linea di comando, come mostrato nel Listato 15-10. Questo task si aspetta come parametri un nome di applicazione ed un nome di test (senza il suffisso Test.php).

Listato 15-10 - Lanciare un test funzionale singolo dalla linea di comando

php symfony test:functional frontend foobarActions
# get /comment/index
ok 1 - status code is 200
ok 2 - request parameter module is foobar
ok 3 - request parameter action is index
not ok 4 - response selector body does not match regex /This is a temporary page/
# Looks like you failed 1 tests of 4.
1..4

I test funzionali generati per un nuovo modulo non passano per default. Ciò in quanto in un nuovo modulo, l'azione index fa un forward ad una pagina di congratulazioni (inclusa nel modulo default di symfony) che contiene la frase "This is a temporary page." Se non modifichi l'azione index, i test per questo modulo falliscono, e ciò garantisce che un modulo non completato passi i test.

NOTE Nei test funzionali l'autoloading è attivo, per cui non devi includere i file a mano.

Browsing con l'oggetto sfTestBrowser

Il browser di test è capace di eseguire richieste in POST ed in GET. In entrambi i casi, usa una URI reale come parametro. Il Listato 15-11 mostra come scrivere chiamate all'oggetto sfTestBrowser per simulare richieste.

Listato 15-11 - Simulare richieste tramite l'oggetto sfTestBrowser

[php]
include(dirname(__FILE__).'/../../bootstrap/functional.php');

// Crea un nuovo browser di test
$b = new sfTestBrowser();
$b->initialize();

$b->get('/foobar/show/id/1');                   // Richiesta in GET
$b->post('/foobar/show', array('id' => 1));     // Richiesta in POST

// I metodi get() e post() sono shortcut al metodo call()
$b->call('/foobar/show/id/1', 'get');
$b->call('/foobar/show', 'post', array('id' => 1));

// Il metodo call() può simulare richieste con qualsiasi metodo
$b->call('/foobar/show/id/1', 'head');
$b->call('/foobar/add/id/1', 'put');
$b->call('/foobar/delete/id/1', 'delete');

Una sessione di browsing tipica non contiene solo richieste ad azioni specifiche, ma anche click su link e sui pulsanti del browser. Come mostrato nel Listato 15-12, l'oggetto sfTestBrowser può simulare anche tali click.

Listato 15-12 - Simulare la navigazione tramite l'oggetto sfTestBrowser

[php]
$b->get('/');                  // Richiesta per la home page
$b->get('/foobar/show/id/1');
$b->back();                    // Indietro di una pagina nella history
$b->forward();                 // Avanti di una pagina nella history
$b->reload();                  // Ricarica la pagina corrente
$b->click('go');               // Cerca il pulsante o link 'go' e lo clicca

Il browser di test gestisce anche stack di chiamate, per cui i metodo back() e forward() funzionano esattamente come nei browser reali.

TIP Il browser di test possiede il proprio meccanismo per gestire le sessioni (sfTestStorage) ed i cookie.

Fra tutte le interazioni che hanno bisogno di essere testate, sicuramente quelle associate alla form sono al primo posto. Per simulare l'input di dati e l'invio della form, hai tre possibilità. Puoi fare una richiesta in POST con i parametri che desideri spedire, chiamare click() con i parametri della form sotto forma di array o riempire i campi a uno a uno e cliccare il pulsante submit. Ad ogni modo, ognuno di essi sfocia in una richiesta in POST. Il Listato 15-13 ne mostra un esempio.

Listato 15-13 - Simulare l'invio di form tramite l'oggetto sfTestBrowser

// Template di esempio in modules/foobar/templates/editSuccess.php
<?php echo form_tag('foobar/update') ?>
  <?php echo input_hidden_tag('id', $sf_params->get('id')) ?>
  <?php echo input_tag('name', 'foo') ?>
  <?php echo submit_tag('go') ?>
  <?php echo textarea('text1', 'foo') ?>
  <?php echo textarea('text2', 'bar') ?>
</form>

// Esempio di test funzionale per questa form
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');

// Opzione 1: richiesta in POST
$b->post('/foobar/update', array('id' => 1, 'name' => 'dummy', 'commit' => 'go'));

// Opzione 2: Clicca il pulsante submit con parametri
$b->click('go', array('name' => 'dummy'));

// Opzione 3: riempi i campi a uno a uno e poi clicca su submit
$b->setField('name', 'dummy')->
    click('go');

NOTE Con la seconda e terza opzione, i valori di default della form sono inclusi automaticamente nella submit, per cui non c'è bisogno di specificare il target.

Quando un'azione finisce con un redirect(), il browser di test non la segue automaticamente; lo devi fare a mano tramite followRedirect(), come mostrato dal Listato 15-14.

Listato 15-14 - Il browser di test non segue automaticamente il forward

// Esempio di azione in modules/foobar/actions/actions.class.php
public function executeUpdate($request)
{
  // ...
  $this->redirect('foobar/show?id='.$request->getParameter('id'));
}

// Esempio di test funzionale per questa azione
$b = new sfTestBrowser();   
$b->get('/foobar/edit?id=1')->
    click('go', array('name' => 'dummy'))->
    isRedirected()->   // Controlla se esiste un forward
    followRedirect();    // Segue il forward

C'è infine un ultimo metodo molto utile di cui dovresti essere a conoscenza: restart() reimposta la history di navigazione, la sessione ed i cookie, proprio come se tu riavviassi il tuo browser.

Dopo aver fatto la prima richiesta, sfTestBrowser fornisce accesso agli oggetti richiesta, contesto e risposta. Significa che puoi controllare molte cose, dal contenuto agli header della risposta, i parametri di richiesta e le configurazioni:

$request  = $b->getRequest();
$context  = $b->getContext();
$response = $b->getResponse();

SIDEBAR L'oggetto sfBrowser

Tutti i metodi di browsing descritti nei Listati dal 15-10 al 15-13 sono anche disponibili al di fuori delle finalità di testing, tramite l'oggetto sfBrowser. Puoi chiamarlo così:

// Crea un nuovo browser $b = new sfBrowser(); $b->initialize(); $b->get('/foobar/show/id/1')-> setField('name', 'dummy')-> click('go'); $content = $b()->getResponse()->getContent(); // ...

L'oggetto sfBrowser è uno strumento molto utile per script batch, ad esempio, se tu volessi navigare una lista di pagine per crearne una versione in cache (consulta il Capitolo 18 per un esempio dettagliato).

Utilizzare asserzioni

Dato che l'oggetto sfTestBrowser ha accesso alla risposta ed agli altri componenti della richiesta, puoi eseguire test su di essi. Potresti creare un nuovo oggetto lime_test a questo scopo, ma fortunatamente sfTestBrowser propone un metodo test() che restituisce un oggetto lime_test dove puoi chiamare i metodi di asserzione descritti precedentemente. Controlla il Listato 15-15 per vedere come fare asserzioni tramite sfTestBrowser.

Listato 15-15 - Il browser di test fornisce funzionalità di testing tramite il metodo test()

$b = new sfTestBrowser();
$b->get('/foobar/edit/id/1');
$request  = $b->getRequest();
$context  = $b->getContext();
$response = $b->getResponse();

// Avere accesso ai metodi lime_test tramite test()
$b->test()->is($request->getParameter('id'), 1);
$b->test()->is($response->getStatuscode(), 200);
$b->test()->is($response->getHttpHeader('content-type'), 'text/html;charset=utf-8');
$b->test()->like($response->getContent(), '/edit/');

NOTE I metodi getResponse(), getContext(), getRequest(), e test() non restituiscono un oggetto sfTestBrowser, perciò dopo di essi non puoi concatenare altre chiamate al metodo sfTestBrowser.

Puoi facilmente controllare cookie entranti ed uscenti tramite gli oggetti richiesta e risposta, come mostrato dal Listato 15-16.

Listato 15-16 - Testare i cookie tramite sfTestBrowser

$b->test()->is($request->getCookie('foo'), 'bar');     // Incoming cookie
$cookies = $response->getCookies();
$b->test()->is($cookies['foo'], 'foo=bar');            // Outgoing cookie

Utilizzare il metodo test() per testare gli elementi della richiesta richiede linee lunghe di codice. Fortunatamente, sfTestBrowser contiene una manciata di metodi proxy che ti aiutano a mantenere i tuoi test funzionali leggibili e corti; inoltre restituiscono a loro volta un oggetto sfTestBrowser. Ad esempio, puoi riscrivere il Listato 15-15 come il Listato 15-17.

Listato 15-17 - Testare direttamente tramite sfTestBrowser

$b = new sfTestBrowser();
$b->get('/foobar/edit/id/1')->
    isRequestParameter('id', 1)->
    isStatutsCode()->
    isResponseHeader('content-type', 'text/html; charset=utf-8')->
    responseContains('edit');

Lo stato 200 è il valore di default previsto da isStatusCode(), per cui puoi chiamare tale metodo senza alcun parametro per testare una risposta che passi con successo.

Un ulteriore vantaggio dei metodi proxy è che non hai bisogno di specificare un testo di output come faresti con i metodi lime_test. I messaggi vengono generati automaticamente dai metodi proxy, e l'output risulta chiaro e leggibile.

# get /foobar/edit/id/1
ok 1 - request parameter "id" is "1"
ok 2 - status code is "200"
ok 3 - response header "content-type" is "text/html"
ok 4 - response contains "edit"
1..4

In pratica, i metodi proxy del Listato 15-17 coprono la maggior parte dei test usuali, per cui utilizzerai raramente il metodo test() di un oggetto sfTestBrowser.

Il Listato 15-14 mostrava che l'oggetto sfTestBrowser non segue automaticamente le redirezioni. Ciò ha un vantaggio: le puoi testare. Per esempio, il Listato 15-18 mostra come testare la risposta del Listato 15-14.

Listato 15-18 - Testare le redirezioni con sfTestBrowser

$b = new sfTestBrowser();
$b->
    get('/foobar/edit/id/1')->
    click('go', array('name' => 'dummy'))->
    isStatusCode(200)->
    isRequestParameter('module', 'foobar')->
    isRequestParameter('action', 'update')->

    isRedirected()->      // Controlla che la risposta è un redirect
    followRedirect()->    // Segue il redirect

    isStatusCode(200)->
    isRequestParameter('module', 'foobar')->
    isRequestParameter('action', 'show');

Utilizzare selettori CSS

Diversi test funzionali convalidano una pagina controllando che sia presente un certo testo all'interno del contenuto. Con l'aiuto del metodo delle espressioni regolari responseContains(), puoi controllare testo visualizzato, attributi dei tag o valori. Ma se tu volessi testare qualcosa profondamente nascosto nel DOM della risposta, le espressioni regolari non sono più il metodo ideale.

Ecco perchè l'oggetto sfTestBrowser supporta il metodo getResponseDom(). Esso restituisce un oggetto libXML2 DOM, molto più facile di un file di testo da parsare e da testare. Nel Listato 15-19 trovi un esempio di utilizzo di questo metodo.

Listato 15-19 - Il browser di test fornisce accesso al contenuto della risposta come oggetto DOM

$b = new sfTestBrowser();
$b->get('/foobar/edit/id/1');
$dom = $b->getResponseDom();
$b->test()->is($dom->getElementsByTagName('input')->item(1)->getAttribute('type'),'text');

Ma fare il parsing di un documento HTML con i metodi DOM di PHP è ancora non abbastanza facile e veloce. Se hai familiarità con i CSS, sai che essi sono un metodo ancora più potente per recuperare elementi da un documento HTML. Symfony fornisce una classe chiamata sfDomCssSelector che si aspetta un documento DOM come parametro del costruttore. Possiede un metodo getTexts() che restituisce un array di stringhe secondo un selettore CSS, ed il metodo getElements() che restituisce un array di elementi DOM. Un esempio nel Listato 15-20.

Listato 15-20 - Il browser di test fornisce accesso al contenuto della risposta come oggetto sfDomCssSelector

$b = new sfTestBrowser();
$b->get('/foobar/edit/id/1');
$c = new sfDomCssSelector($b->getResponseDom())
$b->test()->is($c->getTexts('form input[type="hidden"][value="1"]'), array('');
$b->test()->is($c->getTexts('form textarea[name="text1"]'), array('foo'));
$b->test()->is($c->getTexts('form input[type="submit"]'), array(''));

Nella sua costante ricerca di brevità e chiarezza, symfony fornisce uno shortcut per questo: il metodo proxy checkResponseElement(). Esso fa diventare il Listato 15-20 equivalente al 15-21.

Listato 15-21 - Il browser di test fornisce accesso agli elementi della risposta tramite selettori CSS

$b = new sfTestBrowser();
$b->get('/foobar/edit/id/1')->
    checkResponseElement('form input[type="hidden"][value="1"]', true)->
    checkResponseElement('form textarea[name="text1"]', 'foo')->
    checkResponseElement('form input[type="submit"]', 1);

Il comportamento del metodo checkResponseElement() dipende dal tipo del secondo argomento che riceve: * Se è un booleano, controlla che l'elemento che corrisponde al selettore CSS esista. * Se è un intero, controlla che il selettore CSS restituisca tale numero di risultati. * Se è un'espressione regolare, controlla che corrisponda al primo elemento trovato dal selettore CSS. * Se è un'espressione regolare preceduta da !, controllare che il primo elemento non corrisponda al pattern. * Per gli altri casi, controlla che il primo elemento trovato dal selettore CSS corrisponda al secondo argomento come stringa.

Il metodo accetta un terzo parametro opzionale, nella forma di array associativo. Ti permette di testare non il primo elemento restituito dal selettore (se più di uno) ma un altro ad una certa posizione, come mostrato dal Listato 15-22.

Listato 15-22 - Utilizzare l'opzione di posizione

$b = new sfTestBrowser();
$b->get('/foobar/edit?id=1')->
    checkResponseElement('form textarea', 'foo')->
    checkResponseElement('form textarea', 'bar', array('position' => 1));

L'array opzionale può anche essere usata per far girare due test allo stesso momento. Puoi testare che ci sia un elemento corrispondente ad un selettore e quanti ce ne sono, come mostrato nel Listato 15-23.

Listato 15-23 - Utilizzare l'opzione count per contare le corrispondenze

$b = new sfTestBrowser();
$b->get('/foobar/edit?id=1')->
    checkResponseElement('form input', true, array('count' => 3));

Questo strumento è molto potente. Accetta la maggior parte dei selettori CSS 3, e lo puoi usare per query complesse come quelle del Listato 15-24.

Listato 15-24 - Esempio di selettori CSS complessi accettati da checkResponseElement()

$b->checkResponseElement('ul#list li a[href]', 'click me');
$b->checkResponseElement('ul > li', 'click me');
$b->checkResponseElement('ul + li', 'click me');
$b->checkResponseElement('h1, h2', 'click me');
$b->checkResponseElement('a[class$="foo"][href*="bar.html"]', 'my link');
$b->checkResponseElement('p:last ul:nth-child(2) li:contains("Some text")');

Testare gli errori

A volte le tue azioni o i tuoi modelli sollevano delle eccezioni appositamente (ad esempio per mostrare una pagina 404). Anche se potresti usare un selettore CSS per verificare che uno specifico messaggio di errore sia nel codice HTML generato, è meglio usare il metodo throwsException per verificare che un'eccezione sia stata sollevata, come mostrato nel Listato 15-25.

Listing 15-25 - Testare le eccezioni

[php]
$b = new sfTestBrowser();
$b->
    get('/foobar/edit/id/1')->
    click('go', array('name' => 'dummy'))->
    isStatusCode(200)->
    isRequestParameter('module', 'foobar')->
    isRequestParameter('action', 'update')->

    throwsException()->                   // Checks that the last request threw an exception
    throwsException('RuntimeException')-> // Checks the class of the exception
    throwsException(null, '/error/');     // Checks that the content of the exception message matches the regular expression

Lavorare nell'ambiente di test

L'oggetto sfTestBrowser usa uno speciale front controller, impostato sull'ambiente test. La configurazione di default per questo ambiente è mostrata nel Listato 15-26.

Listato 15-26 - Configurazione di default dell'ambiente test, in myapp/config/settings.yml

test:
  .settings:
    error_reporting:        <?php echo (E_ALL | E_STRICT & ~E_NOTICE)."\n" ?>
    cache:                  off
    web_debug:              off
    no_script_name:         off
    etag:                   off

La cache e la web debug toolbar sono disabilitate in questo ambiente. Comunque, l'esecuzione del codice lascia ancora tracce in un file di log, distinto dai file di log di prod e dev, in modo che lo puoi controllare indipendentemente (myproject/log/myapp_test.log). In questo ambiente, le eccezioni non fermano l'esecuzione degli script, così puoi far girare un intero insieme di test anche se uno fallisce. Puoi avere impostazioni specifiche di connessione al database, ad esempio, per usarne un altro riempito con dati di test.

Prima di usare l'oggetto sfTestBrowser, lo devi inizializzare. Nel caso ne avessi bisogno, potresti specificare un hostname per l'applicazione ed un indirizzo IP per il client, nel caso la tua applicazione controlli questi due parametri. Il Listato 15-27 mostra come fare.

Listato 15-27 - Impostare il test browser con hostname e IP

$b = new sfTestBrowser();
$b->initialize('myapp.example.com', '123.456.789.123');

Il task test:functional

Il task test:functional può far girare uno o più test funzionali, dipendentemente dal numero di argomenti ricevuti. Le regole sembrano quelle del task test:unit, tranne per il fatto che il task test:functional si aspetta sempre il nome di un'applicazione come primo argomento, come mostrato dal Listato 15-28.

Listato 15-28 - Sntassi del task test:functional

// Struttra della cartella `Test`
test/
  functional/
    frontend/
      myModuleActionsTest.php
      myScenarioTest.php
    backend/
      myOtherScenarioTest.php

## Esegue tutti i test funzionali per un'applicazione, ricorsivamente
> php symfony test:functional frontend

## Esegue un dato test funzionale
> php symfony test:functional frontend myScenario

## Esegue diversi test funzionali, secondo un pattern
> php symfony test:functional frontend my*

Pratiche di nomenclatura dei test

Questa sezione elenca qualche buona abitudine per tenere i tuoi test organizzati e facili da manutenere. I suggerimenti riguardano l'organizzazione dei file, i test unitari ed i test funzionali.

Per la struttura dei file, dovresti chiamare i test unitari con il nome delle classi che essi devono testare, ed i test funzionali secondo il modulo o scenario di cui si occupano. Un esempio nel Listato 15-29. La tua cartella test/ conterrà presto molti file, e se non segui queste linee guida potrebbe divenire difficoltoso trovare i file.

Listato 15-29 - Esempio di nomi dei file

test/
  unit/
    myFunctionTest.php
    mySecondFunctionTest.php
    foo/
      barTest.php
  functional/
    frontend/
      myModuleActionsTest.php
      myScenarioTest.php
    backend/
      myOtherScenarioTest.php

Per i test unitari, una buona abitudine è quella di raggruppare i test per funzione o metodo, e cominciare ogni gruppo di test con una chiamata diag(). I messaggi di ogni test unitario dovrebbero contenere il nome della funzione o metodo testato, seguito da un verbo ed una proprietà, in modo che l'output sembri una frase che descriva una proprietà dell'oggetto. Il Listato 15-30 mostra un esempio.

Listato 15-30 - Esempio di nomi dei file per i test unitari

// srttolower()
$t->diag('strtolower()');
$t->isa_ok(strtolower('Foo'), 'string', 'strtolower() returns a string');
$t->is(strtolower('FOO'), 'foo', 'strtolower() transforms the input to lowercase');

# strtolower()
ok 1 - strtolower() returns a string
ok 2 - strtolower() transforms the input to lowercase

I test funzionali dovrebbero essere raggruppati per pagina e cominciare con una richiesta. Il Listato 15-31 illustra questa pratica.

Listato 15-31 - Esempio di nomi di test funzionali

$browser->
  get('/foobar/index')->
  isStatusCode(200)->
  isRequestParameter('module', 'foobar')->
  isRequestParameter('action', 'index')->
  checkResponseElement('body', '/foobar/')
;

# get /comment/index
ok 1 - status code is 200
ok 2 - request parameter module is foobar
ok 3 - request parameter action is index
ok 4 - response selector body matches regex /foobar/

Se segui questa convenzione, l'ouput dei tuoi test sarò chiaro abbastanza da poter essere usato come documentazione di sviluppo; in certi casi chiaro abbastanza da rendere inutile ulteriore documentazione.

Esigenze particolari nei test

Gli strumenti forniti da symfony per i test funzionali e per i test unitari dovrebbero essere sufficienti nella maggior parte dei casi. Qualche funzionalità ulteriore viene qui elencata per risolvere problemi comuni nei test automatici: lanciare test in ambienti isolati, accedere al database senza test, testare la cache ed interazioni di test lato client.

Eseguire test in un test harness

I task test:unit e test:functional possono lanciare un test singolo od un insieme di test. Ma se chiami questi task senza alcun parametro, essi lanciano tutte le unit e functional test nella cartella test/. Viene incluso un meccanismo particolare per isolare ogni test in una sandbox indipendente, al fine di evitare rischi di contaminazione fra test. Inoltre i risultati dei test vengono compattati in viste sintetiche; ecco perchè l'esecuzione di una grande quantità di file di test usa un test harness, ovvero un framework di test automatici con speciali funzionalità. Un test harness si basa su un componente del framework lime chiamato lime_harness. Mostra lo stato dei test file per file, ed alla fine una panoramica del numero dei test passati rispetto al totale, come mostrato nel Listato 15-32.

Listato 15-32 - Lanciare i test in un test harness

php symfony test:unit
unit/myFunctionTest.php................ok
unit/mySecondFunctionTest.php..........ok
unit/foo/barTest.php...................not ok

Failed Test                     Stat  Total   Fail  List of Failed
------------------------------------------------------------------
unit/foo/barTest.php               0      2      2  62 63
Failed 1/3 test scripts, 66.66% okay. 2/53 subtests failed, 96.22% okay.

I test vengono eseguiti nello stesso modo come quando li chiami ad uno ad uno, solo l'ouput viene accorciato per essere davvero utile. In particolare, l'ultima tabella è orientata ai test falliti e ti aiuta a capire quali.

Puoi lanciare tutti i test con una singola chiamata tramite il task test:all, il quale a sua volta usa un test harness, come mostrato nel Listato 15-33. Questa è una cosa suggerita prima del trasferimento in produzione, per assicurarsi che non ci sia alcuna regressione dall'ultimo rilascio.

Listato 15-33 - Lanciare tutti i test di un progetto

php symfony test:all

Accedere ad un database

I test unitari hanno spesso bisogno di accedere ad un db. Una connessione viene inizializzata automaticamente alla prima chiamata di sfTestBrowser::get(). Ad ogni modo, se tu volessi accedervi anche prima tramite sfTestBrowser, dovresti inizializzare l'oggetto sfDabataseManager manualmente, come mostrato nel Listato 15-34.

Listato 15-34 - Inizializzare un db in un test

$databaseManager = new sfDatabaseManager($configuration);
$databaseManager->loadConfiguration();

// Opzionalmente, puoi recuperare la connessione a db corrente
$con = Propel::getConnection();

Prima di cominciare i test dovresti popolare il db con fixture. Ciò può essere fatto tramite l'oggetto sfPropelData. Tale oggetto può caricare dati da un file, proprio come il task propel:data-load, o da un'array, come mostrato nel Listato 15-35.

Listato 15-35 - Popolare un db da un file

$data = new sfPropelData();

// Caricare i dati da un file
$data->loadData(sfConfig::get('sf_data_dir').'/fixtures/test_data.yml');

// Caricare i dati da un array
$fixtures = array(
  'Article' => array(
    'article_1' => array(
      'title'      => 'foo title',
      'body'       => 'bar body',
      'created_at' => time(),
    ),
    'article_2'    => array(
      'title'      => 'foo foo title',
      'body'       => 'bar bar body',
      'created_at' => time(),
    ),
  ),
);
$data->loadDataFromArray($fixtures);

Dopodiché, usa gli oggetti Propel come faresti in un'applicazione normale, secondo i tuoi bisogni di testing. Ricorda di includere i loro file nei test unitari (puoi usare la classe sfSimpleAutoload, come spiegato precedentemente nella sezione "Stub, Fixture e Autoloading"). Gli oggetti Propel vengono auto-caricati nei test funzionali.

Testare la cache

Quando abiliti la cache per un'applicazione, i test funzionali dovrebbero verificare che le azioni in cache funzionino come ci si aspetta.

La prima cosa da fare è abilitare la cache nell'ambiente test (nel file settings.yml). Quindi, qualora tu voglia testare quando una pagina arriva dalla cache o quando viene generata, devi usare il metodo isCached() fornito dall'oggetto sfTestBrowser. Il Listato 15-36 mostra questo metodo.

Listato 15-36 - Testare la cache con il metodo isCached()

<?php

include(dirname(__FILE__).'/../../bootstrap/functional.php');

// Create a new test browser
$b = new sfTestBrowser();
$b->initialize();

$b->get('/mymodule');
$b->isCached(true);       // Controlla che la risposta venga dalla cache
$b->isCached(true, true); // Controlla che la risposta in cache arrivi con layout
$b->isCached(false);      // Controlla che la risposta non venga dalla cache

NOTE Non hai bisogno di pulire la cache all'inizio di un test funzionale; lo script di bootstrap lo fa per te.

Testare interazioni lato client

Il più grande svantaggio delle tecniche mostrate finora è che esse non possono simulare JavaScript. Per interazioni molto complesse, ad esempio come quelle Ajax, hai bisogno di riprodurre esattamente il mouse e gli input da tastiera di un eventuale utente, nonchè l'esecuzione di script lato client. Di solito questi test vengono riprodotti a mano, ma sono una vera perdita di tempo e soggetti a errori.

La soluzione si chiama Selenium ([http://www.openqa.org/selenium/]), che è un framework di test scritto interamente in JavaScript. Esegue una serie di azioni nella pagina proprio come farebbe un utente normale, utilizzando la finestra del browser corrente. Il vantaggio rispetto a sfBrowser è che Selenium è in grado di simulare JavaScript, per cui puoi anche testare interazioni Ajax.

Selenium non è incluso in symfony per default. Per installarlo, devi creare una cartella selenium/ all'interno della cartella web/, e scompattarci l'archivio di Selenium ([http://www.openqa.org/selenium-core/download.action]). Questo perchè Selenium si basa su JavaScript, e dato che le impostazioni di sicurezza di default di molti browser ne bloccano l'esecuzione a meno che non sia disponibile sullo stesso host e sulla stessa porta della tua applicazione.

CAUTION Fai attenzione a non copiare la cartella selenium/ sul server in produzione, in quanto risulterebbe accessibile a chiunque dalla webroot.

I test di Selenium sono scritti in HTML e memorizzati nella cartella web/selenium/tests/. Ad esempio, il Listato 15-37 mostra un test funzionale dove viene caricata la home page, viene cliccato il link "click me" e viene cercato nella risposta il testo "Hello, World". Ricorda che per poter accedere all'applicazione nell'ambiente test, devi specificare il front controller myapp_test.php.

Listato 15-37 - Esempio di test Selenium, in web/selenium/test/testIndex.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <meta content="text/html; charset=UTF-8" http-equiv="content-type">
  <title>Index tests</title>
</head>
<body>
<table cellspacing="0">
<tbody>
  <tr><td colspan="3">First step</td></tr>
  <tr><td>open</td>              <td>/frontend_test.php/</td> <td>&nbsp;</td></tr>
  <tr><td>clickAndWait</td>      <td>link=click me</td>    <td>&nbsp;</td></tr>
  <tr><td>assertTextPresent</td> <td>Hello, World!</td>    <td>&nbsp;</td></tr>
</tbody>
</table>
</body>
</html>

Un caso di test è rappresentato da un documento HTML che contiene una tabella con tre colonne: comando, target e valore. Non tutti i comandi hanno un valore, comunque. In questo caso, lascia la colonna vuota o usa &nbsp; perchè la tabella goda comunque di un buon look. Consulta il sito di Selenium per una lista completa dei comandi.

Hai anche bisogno di aggiungere questo test all'insieme globale inserendo una nuova linea nella tabella del file TestSuite.html, situato nella stessa cartella. Il Listato 15-38 mostra come.

Listato 15-38 - Aggiungere un file di test alla suite di test, in web/selenium/test/TestSuite.html

... My First Test ...

Per eseguire il test, naviga semplicemente su

http://myapp.example.com/selenium/index.html

Seleziona Main Test Suite, clicca il pulsante per eseguire tutti i test, e guarda il browser riprodurre gli step che tu gli hai detto di eseguire.

NOTE Dato che i test Selenium girano su un browser reale, ti permettono anche di testarne le incoerenze. Costruisci i tuoi test con un browser, e poi testali con tutti quelli con cui il tuo sito dovrebbe funzionare.

Il fatto che i test Selenium siano scritti in HTML potrebbe rendere una seccatura il loro procedimento di scrittura. Ma grazie all'estensione Firefox Selenium ([http://selenium-ide.openqa.org/]), tutto ciò di cui hai bisogno per creare un test è di eseguirlo una volta in una sessione registrata. Finché navighi nella stessa sessione, puoi aggiungere test come tipi di asserzioni semplicemente cliccando con il tasto destro nella finestra del browser e selezionando il check giusto nel menu Append Selenium Command.

Puoi salvare il test inun file HTML per costruire una suite di test per la tua applicazione. L'estensione per Firefox ti permette anche di eseguire test Selenium registrati precedentemente.

NOTE Non dimenticare di reinizializzare i dati di test prima di lanciare i test Selenium.

Sommario

I test automatici includono test unitari per convalidare metodi o funzioni e test funzionali per convalidare funzionalità. Symfony si basa sul framework di testing lime per i test unitari e fornisce una classe speciale sfTestBrowser per i test funzionali. Entrambi mettono a disposizione molti metodi di asserzione, da quelli base fino ai più avanzati, come i selettori CSS. Usa la linea di comandi per lanciare i test, uno ad uno (con i task test:unit e test:functional) o in un test harness (con il task test:all). Quando gestisci i dati, i test automatici usano fixture e stub, e questo è facilmente eseguibile con i test unitari di symfony.

Se ti assicuri di scrivere abbastanza test unitari da coprire gran parte della tua applicazione (usando la metodologia TDD), ti sentirai al sicuro durante le operazioni di refactoring o di aggiunta di nuove funzionalità, e potresti anche guadagnare tempo sulla documentazione.