Development

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

You must first sign up to be able to contribute.

Capitolo 15 - Unit test e test 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 nella distribuzione standard di symfony, ma li puoi scaricare tramite SVN o navigare all'indirizzo http://www.symfony-project.com/trac/browser/trunk/test.

Unit e functional test

Le unit test 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.

Le unit test 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 functional test 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.

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'assezione 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. E' 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 underscore 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 le unit test, le 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.

Fare il refactoring di 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. E' 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 le unit testing. E' 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 unit e functional test nel suo codice sorgente.
  • Il core di lime è convalidato da unit test.
  • E' scritto in PHP, ed è veloce e scritto bene. E' 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.

Le unit e functional test non sono pensate per essere lanciate in produzione. Si tratta di strumenti di sviluppo, e come tali dovrebbero essere usati sulla macchina dello sviluppatore e non sul server di produzione.

Unit Test

Le unit test 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 unit test per la funzione strtolower(). Comincia con l'istanziazione dell'oggetto lime_test (non ti preoccupare dei parametri per ora). Ogni unit test è 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 unit test dalla linea di comando

> 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.

Lo statement 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 delle Unit Test

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

Tabella 15-2 - Metodi dell'oggetto lime_test per le unit testing

Method Description
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.

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

> 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 le unit test dalla 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, ficture e autoloading

In una unit test, 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');
include(dirname(__FILE__).'/../../config/config.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');

Nelle unit test, 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:

require_once($sf_symfony_lib_dir.'/util/sfCore.class.php');
sfCore::initSimpleAutoload($sf_symfony_lib_dir.'/util');

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, le unit test 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

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 in unit test

// 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');
include(dirname(__FILE__).'/../../config/config.php');
require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');
require_once($sf_symfony_lib_dir.'/util/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']);
}

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 init-module o propel-init-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->initialize();
 
$browser->
  get('/foobar/index')->
  isStatusCode(200)->
  isRequestParameter('module', 'foobar')->
  isRequestParameter('action', 'index')->
  checkResponseElement('body', '!/This is a temporary page/')
;

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

> 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.

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

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

$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.

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');

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()
{
  ...
  $this->redirect('foobar/show?id='.$this->getRequestParameter('id'));
}
 
// Esempio di test funzionale per questa azione
$b = new sfTestBrowser();   
$b->initialize();
$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();

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->initialize();
$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/');

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->initialize();
$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->initialize();
$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->initialize();
$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->initialize();
$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->initialize();
$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->initialize();
$b->get('/foobar/edit?id=1')->
    checkResponseElement('form textarea', 'foo')->
    checkResponseElement('form textarea', 'bar', array('position' => 1));

L'array opzionale può anche essere usato 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->initialize();
$b->get('/foobar/edit?id=1')->
    checkResponseElement('form input', true, array('count' => 3));

Questo strumento è molto potente. Accetta la maggior parte dei selettori CSS 2.1, 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');

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-25.

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

test:
  .settings:
    # E_ALL | E_STRICT & ~E_NOTICE = 2047
    error_reporting:        2047
    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-26 mostra come fare.

Listato 15-26 - 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-27.

Listato 15-27 - 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
> symfony test-functional frontend

## Esegue un dato test funzionale
> symfony test-functional frontend myScenario

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

Pratiche di naming 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, le unit test ed i test funzionali.

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

Listato 15-28 - Esempio di nomi dei file

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

Per le unit test, 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 unit test 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-29 mostra un esempio.

Listato 15-29 - Esempio di nomi dei file per le test unit

// 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-30 illustra questa pratica.

Listato 15-30 - 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.

Speciali bisogni nei test

Gli strumenti forniti da symfony per i test funzionali e per le unit test 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 db 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-31.

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

> 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-32. Questa è una cosa suggerita prima del trasferimento in produzione, per assicurarsi che non ci sia alcuna regressione dall'ultimo rilascio.

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

> symfony test-all

Accedere ad un database

Le unit test 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-33.

Listato 15-33 - Inizializzare un db in un test

$databaseManager = new sfDatabaseManager();
$databaseManager->initialize();
 
// 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-load-data, o da un array, come mostrato nel Listato 15-34.

Listato 15-34 - 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 nelle unit test (puoi usare il metodo sfCore::sfSimpleAutoloading(), 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-35 mostra questo metodo.

Listato 15-35 - 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

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.

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-36 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-36 - 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>/myapp_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-37 mostra come.

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

...
<tr><td><a href='./testIndex.html'>My First Test</a></td></tr>
...

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.

Dato che i test Selenium girano su un browser reale, ti permettono anche di testarne le inconsistenze. 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.

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

Sommario

I test automatici includono unit test per convalidare metodi o funzioni e test funzionali per convalidare funzionalità. Symfony si basa sul framework di testing lime per le unit test 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 le unit test di symfony.

Se ti assicuri di scrivere abbastanza unit test 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.