Test di scrittura

La scrittura di test per Nette Tester è unica, in quanto ogni test è uno script PHP che può essere eseguito in modo indipendente. Questo ha un grande potenziale. Quando si scrive il test, è possibile eseguirlo semplicemente per vedere se funziona correttamente. In caso contrario, si può facilmente passare all'IDE e cercare un bug.

Si può anche aprire il test in un browser. Ma soprattutto, eseguendo il test, lo si esegue. Scoprirete immediatamente se è stato superato o meno.

Nel capitolo introduttivo, abbiamo mostrato un test davvero banale sull'uso degli array in PHP. Ora creeremo la nostra classe, che testeremo, anche se sarà anch'essa semplice.

Cominciamo con una tipica disposizione delle cartelle di una libreria o di un progetto. È importante separare i test dal resto del codice, ad esempio per la distribuzione, perché non vogliamo caricare i test sul server. La struttura può essere la seguente:

├── src/           # code that we will test
│   ├── Rectangle.php
│   └── ...
├── tests/         # tests
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

Ora creeremo i singoli file. Inizieremo con la classe testata, che collocheremo 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('The dimension must not be negative.');
		}
		$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 creeremo un test per essa. Il nome del file di test deve corrispondere alla maschera *Test.php o *.phpt, noi sceglieremo la variante RectangleTest.php:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// oblungo generico
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # verificheremo i risultati attesi
Assert::false($rect->isSquare());

Come si può vedere, i metodi di asserzione come Assert::same() sono usati per affermare che un valore reale corrisponde a un valore atteso.

L'ultimo passo consiste nel creare il file bootstrap.php. Esso contiene un codice comune per tutti i test. Ad esempio, il caricamento automatico delle classi, la configurazione dell'ambiente, la creazione di una cartella temporanea, gli helper e simili. Ogni test carica il bootstrap e presta attenzione solo ai test. Il bootstrap può essere simile a:

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

Questo bootstrap presuppone che l'autoloader di Composer sia in grado di caricare anche le classi Rectangle.php. Ciò può essere ottenuto, ad esempio, impostando la sezione autoload in composer.json, ecc.

Ora possiamo eseguire il test dalla riga di comando come qualsiasi altro script PHP autonomo. La prima esecuzione rivelerà eventuali errori di sintassi e, se non si è fatto un errore di battitura, si vedrà:

$ php RectangleTest.php

OK

Se nel test cambiamo l'affermazione in false Assert::same(123, $rect->getArea());, si verificherà quanto segue:

$ php RectangleTest.php

Failed: 200.0 should be 123

in RectangleTest.php(5) Assert::same(123, $rect->getArea());

FAILURE

Quando si scrivono i test, è bene catturare tutte le situazioni estreme. Ad esempio, se l'input è zero, un numero negativo, in altri casi una stringa vuota, null, ecc. In effetti, questo costringe a pensare e a decidere come il codice dovrebbe comportarsi in queste situazioni. I test poi correggono il comportamento.

Nel nostro caso, un valore negativo dovrebbe lanciare un'eccezione, che verifichiamo con Assert::exception():

// la larghezza non deve essere un numero negativo
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'La dimensione non deve essere negativa',
);

E aggiungiamo un test simile per l'altezza. Infine, verifichiamo che isSquare() restituisca true se entrambe le dimensioni sono uguali. Provate a scrivere questi test come esercizio.

Test ben organizzati

Le dimensioni del file di test possono aumentare e diventare rapidamente ingombranti. Pertanto, è pratico raggruppare le singole aree testate in funzioni separate.

Per prima cosa, mostreremo una variante più semplice ma elegante, utilizzando la funzione globale test(). Il tester non la crea automaticamente, per evitare collisioni nel caso in cui si abbia una funzione con lo stesso nome nel codice. Viene creata solo dal metodo setupFunctions(), che si chiama nel file bootstrap.php:

Tester\Environment::setup();
Tester\Environment::setupFunctions();

Utilizzando questa funzione, possiamo suddividere il file di test in unità denominate. Quando viene eseguita, le etichette vengono visualizzate una dopo l'altra.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('general oblong', function () {
	$rect = new Rectangle(10, 20);
	Assert::same(200.0, $rect->getArea());
	Assert::false($rect->isSquare());
});

test('general square', function () {
	$rect = new Rectangle(5, 5);
	Assert::same(25.0, $rect->getArea());
	Assert::true($rect->isSquare());
});

test('dimensions must not be negative', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

	Assert::exception(
		fn() => new Rectangle(10, -1),
        InvalidArgumentException::class,
	);
});

Se è necessario eseguire il codice prima o dopo ogni test, passarlo a setUp() o tearDown():

setUp(function () {
	// codice di inizializzazione da eseguire prima di ogni test()
});

La seconda variante è l'oggetto. Creeremo il cosiddetto TestCase, che è una classe in cui le singole unità sono rappresentate da metodi il cui nome inizia 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);
	}
}

// Eseguire i metodi di test
(new RectangleTest)->run();

Questa volta abbiamo utilizzato l'annotazione @throw per verificare la presenza di eccezioni. Per ulteriori informazioni, si veda il capitolo TestCase.

Funzioni Helpers

Nette Tester include diverse classi e funzioni che possono facilitare l'esecuzione dei test, ad esempio gli helper per testare il contenuto di un documento HTML, per testare le funzioni di lavoro con i file e così via.

Una loro descrizione è disponibile alla pagina Helpers.

Annotazione e salto dei test

L'esecuzione dei test può essere influenzata dalle annotazioni nel commento phpDoc all'inizio del file. Per esempio, potrebbe essere così:

/**
 * @phpExtension pdo, pdo_pgsql
 * @phpVersion >= 7.2
 */

Le annotazioni dicono che il test deve essere eseguito solo con PHP versione 7.2 o superiore e se sono presenti le estensioni PHP pdo e pdo_pgsql. Queste annotazioni sono controllate dal test runner a riga di comando che, se le condizioni non sono soddisfatte, salta il test e lo contrassegna con la lettera s – saltato. Tuttavia, non hanno alcun effetto quando il test viene eseguito manualmente.

Per una descrizione delle annotazioni, vedere Annotazioni dei test.

Il test può anche essere saltato in base a una propria condizione con Environment::skip(). Ad esempio, salteremo questo test su Windows:

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
	Tester\Environment::skip('Requires UNIX.');
}

Struttura delle directory

Per librerie o progetti solo leggermente più grandi, si consiglia di dividere la directory di test in sottodirectory in base allo spazio dei nomi della classe testata:

└── tests/
	├── NamespaceOne/
	│   ├── MyClass.getUsers.phpt
	│   ├── MyClass.setUsers.phpt
	│   └── ...
	│
	├── NamespaceTwo/
	│   ├── MyClass.creating.phpt
	│   ├── MyClass.dropping.phpt
	│   └── ...
	│
	├── bootstrap.php
	└── ...

In questo modo sarà possibile eseguire i test da un singolo spazio dei nomi, ovvero da una sottodirectory:

tester tests/NamespaceOne

Casi limite

Un test che non chiama alcun metodo di asserzione è sospetto e sarà valutato come errato:

Error: This test forgets to execute an assertion.

Se il test senza chiamare le asserzioni deve essere considerato valido, chiamare ad esempio Assert::true(true).

Può anche essere insidioso usare exit() e die() per terminare il test con un messaggio di errore. Ad esempio, exit('Error in connection') termina il test con un codice di uscita 0, che segnala il successo. Utilizzare Assert::fail('Error in connection').