Scrivere test
Scrivere test per Nette Tester è unico nel senso che ogni test è uno script PHP che può essere eseguito separatamente. Questo nasconde un grande potenziale. Già mentre scrivete il test, potete semplicemente eseguirlo e scoprire se funziona correttamente. In caso contrario, potete facilmente eseguirlo passo passo nell'IDE e cercare l'errore.
Potete persino aprire il test nel browser. Ma soprattutto – eseguendolo, eseguite il test. Scoprite immediatamente se è passato o fallito.
Nel capitolo introduttivo abbiamo mostrato un test davvero banale sul lavoro con un array. Ora creeremo la nostra classe che testeremo, anche se sarà anch'essa semplice.
Iniziamo dalla tipica struttura di directory per una libreria o un progetto. È importante separare i test dal resto del codice, ad esempio per il deployment, perché non vogliamo caricare i test sul server di produzione. La struttura può essere ad esempio così:
├── src/ # codice che testeremo
│ ├── Rectangle.php
│ └── ...
├── tests/ # test
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
E ora creiamo i singoli file. Iniziamo dalla classe testata, che posizioneremo nel file src/Rectangle.php
<?php
class Rectangle
{
private float $width;
private float $height;
public function __construct(float $width, float $height)
{
if ($width < 0 || $height < 0) {
throw new InvalidArgumentException('La dimensione non deve essere negativa.');
}
$this->width = $width;
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
public function isSquare(): bool
{
return $this->width === $this->height;
}
}
E creiamo un test per essa. Il nome del file con il test dovrebbe corrispondere alla maschera *Test.php
o
*.phpt
, scegliamo ad esempio la variante RectangleTest.php
:
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
// rettangolo generico
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # verifichiamo i risultati attesi
Assert::false($rect->isSquare());
Come vedete, i cosiddetti metodi di asserzione come
Assert::same()
vengono utilizzati per confermare che il valore effettivo corrisponde al valore atteso.
Resta l'ultimo passo, ovvero il file bootstrap.php
. Questo contiene il codice comune a tutti i test, ad esempio
l'autoloading delle classi, la configurazione dell'ambiente, la creazione di una directory temporanea, funzioni di aiuto e simili.
Tutti i test caricano il bootstrap e si dedicano poi solo al test. Il bootstrap può apparire come segue:
<?php
require __DIR__ . '/../vendor/autoload.php'; # carica l'autoloader di Composer
Tester\Environment::setup(); # inizializzazione di Nette Tester
// e altre configurazioni (è solo un esempio, nel nostro caso non sono necessarie)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
Il bootstrap indicato presuppone che l'autoloader di Composer sia in grado di caricare anche la classe
Rectangle.php
. Ciò può essere ottenuto ad esempio impostando la sezione autoload in
composer.json
ecc.
Possiamo ora eseguire il test dalla riga di comando come qualsiasi altro script PHP autonomo. La prima esecuzione ci rivelerà eventuali errori di sintassi e se non ci sono errori di battitura da nessuna parte, verrà visualizzato:
$ php RectangleTest.php
OK
Se cambiassimo nel test l'asserzione in una falsa Assert::same(123, $rect->getArea());
succederebbe questo:
$ php RectangleTest.php Failed: 200.0 should be 123 in RectangleTest.php(5) Assert::same(123, $rect->getArea()); FAILURE
Durante la scrittura dei test è bene coprire tutte le situazioni limite. Ad esempio, quando l'input sarà zero, un numero negativo, in altri casi ad esempio una stringa vuota, null ecc. In realtà vi costringe a riflettere e decidere come il codice dovrebbe comportarsi in tali situazioni. I test poi fissano il comportamento.
Nel nostro caso, un valore negativo deve lanciare un'eccezione, cosa che verifichiamo tramite Assert::exception():
// la larghezza non deve essere negativa
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
'La dimensione non deve essere negativa.',
);
E aggiungiamo un test simile per l'altezza. Infine, testiamo che isSquare()
restituisca true
se
entrambe le dimensioni sono uguali. Provate a scrivere tali test come esercizio.
Test più chiari
La dimensione del file con il test può aumentare e diventare rapidamente poco chiara. Pertanto, è pratico raggruppare le singole aree testate in funzioni separate.
Prima mostreremo una variante più semplice, ma elegante, ovvero tramite la funzione globale test()
. Tester non la
crea automaticamente per evitare collisioni nel caso aveste nel codice una funzione con lo stesso nome. La crea invece il metodo
setupFunctions()
, che chiamate nel file bootstrap.php
:
Tester\Environment::setup();
Tester\Environment::setupFunctions();
Tramite questa funzione possiamo suddividere piacevolmente il file di test in unità nominate. All'esecuzione verranno visualizzate progressivamente le descrizioni.
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
test('rettangolo generico', function () {
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
});
test('quadrato generico', function () {
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
});
test('le dimensioni non devono essere negative', function () {
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
);
Assert::exception(
fn() => new Rectangle(10, -1),
InvalidArgumentException::class,
);
});
Se avete bisogno di eseguire codice prima o dopo ogni test, passatelo alla funzione setUp()
rispettivamente
tearDown()
:
setUp(function () {
// codice di inizializzazione che viene eseguito prima di ogni test()
});
La seconda variante è orientata agli oggetti. Creiamo un cosiddetto TestCase, che è una classe in cui le singole unità rappresentano metodi i cui nomi iniziano con test–.
class RectangleTest extends Tester\TestCase
{
public function testGeneralOblong()
{
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
}
public function testGeneralSquare()
{
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
}
/** @throws InvalidArgumentException */
public function testWidthMustNotBeNegative()
{
$rect = new Rectangle(-1, 20);
}
/** @throws InvalidArgumentException */
public function testHeightMustNotBeNegative()
{
$rect = new Rectangle(10, -1);
}
}
// Esecuzione dei metodi di test
(new RectangleTest)->run();
Per testare le eccezioni abbiamo usato questa volta l'annotazione @throws
. Maggiori informazioni nel capitolo TestCase.
Funzioni di aiuto
Nette Tester contiene diverse classi e funzioni che possono facilitarvi, ad esempio, il test del contenuto di un documento HTML, il test di funzioni che lavorano con file e così via.
La loro descrizione la trovate nella pagina Classi di aiuto.
Annotazioni e salto dei test
L'esecuzione dei test può essere influenzata da annotazioni sotto forma di commento phpDoc all'inizio del file. Può apparire ad esempio così:
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
Le annotazioni indicate dicono che il test deve essere eseguito solo con la versione PHP 7.2 o superiore e se sono presenti
le estensioni PHP pdo e pdo_pgsql. Queste annotazioni sono seguite dall'esecutore di test dalla riga di comando, che nel caso in cui le condizioni
non siano soddisfatte, salta il test e nell'output lo contrassegna con la lettera s
– skipped. Tuttavia,
all'esecuzione manuale del test non hanno alcun effetto.
La descrizione delle annotazioni la trovate nella pagina Annotazioni dei test.
È possibile far saltare un test anche in base al soddisfacimento di una condizione personalizzata tramite
Environment::skip()
. Ad esempio, questo salta i test su Windows:
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Richiede UNIX.');
}
Struttura delle directory
Consigliamo, per librerie o progetti anche solo leggermente più grandi, di suddividere ulteriormente la directory con i test in sottodirectory secondo il namespace della classe testata:
└── tests/
├── NamespaceOne/
│ ├── MyClass.getUsers.phpt
│ ├── MyClass.setUsers.phpt
│ └── ...
│
├── NamespaceTwo/
│ ├── MyClass.creating.phpt
│ ├── MyClass.dropping.phpt
│ └── ...
│
├── bootstrap.php
└── ...
Potrete così eseguire i test da un singolo namespace ovvero sottodirectory:
tester tests/NamespaceOne
Situazioni speciali
Un test che non chiama nemmeno un metodo di asserzione è sospetto e viene valutato come errato:
Errore: Questo test dimentica di eseguire un'asserzione.
Se davvero un test senza chiamate di asserzioni deve essere considerato valido, chiamate ad esempio
Assert::true(true)
.
Può anche essere insidioso usare exit()
e die()
per terminare un test con un messaggio di errore. Ad
esempio exit('Errore nella connessione')
termina il test con il codice di ritorno 0, che segnala successo. Usate
Assert::fail('Errore nella connessione')
.