Asserzioni

Le asserzioni vengono utilizzate per confermare che il valore effettivo corrisponde al valore atteso. Si tratta di metodi della classe Tester\Assert.

Scegliete le asserzioni più appropriate. È meglio Assert::same($a, $b) che Assert::true($a === $b), perché in caso di fallimento visualizza un messaggio di errore significativo. Nel secondo caso, solo false should be true che non ci dice nulla sul contenuto delle variabili $a e $b.

La maggior parte delle asserzioni può anche avere una descrizione opzionale nel parametro $description, che viene visualizzata nel messaggio di errore se l'aspettativa fallisce.

Gli esempi presuppongono la creazione di un alias:

use Tester\Assert;

Assert::same ($expected, $actual, ?string $description=null)

$expected deve essere identico a $actual. Lo stesso dell'operatore PHP ===.

Assert::notSame ($expected, $actual, ?string $description=null)

L'opposto di Assert::same(), quindi lo stesso dell'operatore PHP !==.

Assert::equal ($expected, $actual, ?string $description=null, bool $matchOrder=false, bool $matchIdentity=false)

$expected deve essere uguale a $actual. A differenza di Assert::same(), si ignora l'identità degli oggetti, l'ordine delle coppie chiave ⇒ valore negli array e numeri decimali marginalmente diversi, il che può essere modificato impostando $matchIdentity e $matchOrder.

I seguenti casi sono uguali dal punto di vista di equal(), ma non di same():

Assert::equal(0.3, 0.1 + 0.2);
Assert::equal($obj, clone $obj);
Assert::equal(
	['first' => 11, 'second' => 22],
	['second' => 22, 'first' => 11],
);

Attenzione però, gli array [1, 2] e [2, 1] non sono uguali, perché differisce solo l'ordine dei valori, non delle coppie chiave ⇒ valore. L'array [1, 2] può essere scritto anche come [0 => 1, 1 => 2] e quindi sarà considerato uguale [1 => 2, 0 => 1].

Inoltre, in $expected è possibile utilizzare le cosiddette aspettative.

Assert::notEqual ($expected, $actual, ?string $description=null)

L'opposto di Assert::equal().

Assert::contains ($needle, string|array $actual, ?string $description=null)

Se $actual è una stringa, deve contenere la sottostringa $needle. Se è un array, deve contenere l'elemento $needle (il confronto è rigoroso).

Assert::notContains ($needle, string|array $actual, ?string $description=null)

L'opposto di Assert::contains().

Assert::hasKey (string|int $needle, array $actual, ?string $description=null)

$actual deve essere un array e deve contenere la chiave $needle.

Assert::notHasKey (string|int $needle, array $actual, ?string $description=null)

$actual deve essere un array e non deve contenere la chiave $needle.

Assert::true ($value, ?string $description=null)

$value deve essere true, ovvero $value === true.

Assert::truthy ($value, ?string $description=null)

$value deve essere truthy, ovvero soddisfa la condizione if ($value) ....

Assert::false ($value, ?string $description=null)

$value deve essere false, ovvero $value === false.

Assert::falsey ($value, ?string $description=null)

$value deve essere falsy, ovvero soddisfa la condizione if (!$value) ....

Assert::null ($value, ?string $description=null)

$value deve essere null, ovvero $value === null.

Assert::notNull ($value, ?string $description=null)

$value non deve essere null, ovvero $value !== null.

Assert::nan ($value, ?string $description=null)

$value deve essere Not a Number. Per testare il valore NAN, utilizzare esclusivamente Assert::nan(). Il valore NAN è molto specifico e le asserzioni Assert::same() o Assert::equal() possono funzionare in modo imprevisto.

Assert::count ($count, Countable|array $value, ?string $description=null)

Il numero di elementi in $value deve essere $count. Ovvero lo stesso di count($value) === $count.

Assert::type (string|object $type, $value, ?string $description=null)

$value deve essere del tipo specificato. Come $type possiamo usare una stringa:

  • array
  • list – array indicizzato secondo una serie crescente di chiavi numeriche a partire da zero
  • bool
  • callable
  • float
  • int
  • null
  • object
  • resource
  • scalar
  • string
  • nome della classe o direttamente l'oggetto, quindi $value deve essere instanceof $type

Assert::exception (callable $callable, string $class, ?string $message=null, $code=null)

Alla chiamata di $callable deve essere lanciata un'eccezione della classe $class. Se specifichiamo $message, anche il messaggio dell'eccezione deve corrispondere al pattern e se specifichiamo $code, anche i codici devono corrispondere rigorosamente.

Il seguente test fallirà perché il messaggio dell'eccezione non corrisponde:

Assert::exception(
	fn() => throw new App\InvalidValueException('Zero value'),
	App\InvalidValueException::class,
	'Value is to low',
);

Assert::exception() restituisce l'eccezione lanciata, è quindi possibile testare anche un'eccezione annidata.

$e = Assert::exception(
	fn() => throw new MyException('Something is wrong', 0, new RuntimeException),
	MyException::class,
	'Something is wrong',
);

Assert::type(RuntimeException::class, $e->getPrevious());

Assert::error (string $callable, int|string|array $type, ?string $message=null)

Controlla che la funzione $callable abbia generato gli errori attesi (cioè warning, notice, ecc.). Come $type indichiamo una delle costanti E_..., ad esempio E_WARNING. E se specifichiamo $message, anche il messaggio di errore deve corrispondere al pattern. Ad esempio:

Assert::error(
	fn() => $i++,
	E_NOTICE,
	'Undefined variable: i',
);

Se il callback genera più errori, dobbiamo aspettarceli tutti nell'ordine esatto. In tal caso, passiamo un array in $type:

Assert::error(function () {
	$a++;
	$b++;
}, [
	[E_NOTICE, 'Undefined variable: a'],
	[E_NOTICE, 'Undefined variable: b'],
]);

Se come $type indicate il nome di una classe, si comporta come Assert::exception().

Assert::noError (callable $callable)

Controlla che la funzione $callable non abbia generato alcun warning, errore o eccezione. È utile per testare porzioni di codice dove non c'è nessun'altra asserzione.

Assert::match (string $pattern, $actual, ?string $description=null)

$actual deve corrispondere al pattern $pattern. Possiamo usare due varianti di pattern: espressioni regolari o caratteri jolly.

Se come $pattern passiamo un'espressione regolare, per delimitarla dobbiamo usare ~ o #, altri delimitatori non sono supportati. Ad esempio, un test in cui $var deve contenere solo cifre esadecimali:

Assert::match('#^[0-9a-f]$#i', $var);

La seconda variante è simile al confronto di stringhe comune, ma in $pattern possiamo usare diversi caratteri jolly:

  • %a% uno o più caratteri, eccetto i caratteri di fine riga
  • %a?% zero o più caratteri, eccetto i caratteri di fine riga
  • %A% uno o più caratteri, inclusi i caratteri di fine riga
  • %A?% zero o più caratteri, inclusi i caratteri di fine riga
  • %s% uno o più spazi bianchi, eccetto i caratteri di fine riga
  • %s?% zero o più spazi bianchi, eccetto i caratteri di fine riga
  • %S% uno o più caratteri, eccetto gli spazi bianchi
  • %S?% zero o più caratteri, eccetto gli spazi bianchi
  • %c% qualsiasi carattere singolo, eccetto il carattere di fine riga
  • %d% una o più cifre
  • %d?% zero o più cifre
  • %i% valore intero con segno
  • %f% numero con virgola mobile
  • %h% una o più cifre esadecimali
  • %w% uno o più caratteri alfanumerici
  • %% il carattere %

Esempi:

# Di nuovo test per numero esadecimale
Assert::match('%h%', $var);

# Generalizzazione del percorso del file e del numero di riga
Assert::match('Error in file %a% on line %i%', $errorMessage);

Assert::matchFile (string $file, $actual, ?string $description=null)

L'asserzione è identica a Assert::match(), ma il pattern viene caricato dal file $file. Questo è utile per testare stringhe molto lunghe. Il file con il test rimane leggibile.

Assert::fail (string $message, $actual=null, $expected=null)

Questa asserzione fallisce sempre. A volte è semplicemente utile. Opzionalmente possiamo indicare anche il valore atteso e quello attuale.

Aspettative

Quando vogliamo confrontare strutture più complesse con elementi non costanti, le asserzioni sopra menzionate potrebbero non essere sufficienti. Ad esempio, testiamo un metodo che crea un nuovo utente e restituisce i suoi attributi come array. Non conosciamo il valore dell'hash della password, ma sappiamo che deve essere una stringa esadecimale. E di un altro elemento sappiamo solo che deve essere un oggetto DateTime.

In queste situazioni possiamo usare Tester\Expect all'interno del parametro $expected dei metodi Assert::equal() e Assert::notEqual(), con cui è possibile descrivere facilmente la struttura.

use Tester\Expect;

Assert::equal([
	'id' => Expect::type('int'),                   // ci aspettiamo un numero intero
	'username' => 'milo',
	'password' => Expect::match('%h%'),            // ci aspettiamo una stringa che corrisponda al pattern
	'created_at' => Expect::type(DateTime::class), // ci aspettiamo un'istanza della classe
], User::create(123, 'milo', 'RandomPaSsWoRd'));

Con Expect possiamo eseguire quasi le stesse asserzioni di Assert. Quindi abbiamo a disposizione i metodi Expect::same(), Expect::match(), Expect::count() ecc. Inoltre, possiamo concatenarli:

Expect::type(MyIterator::class)->andCount(5);  // ci aspettiamo MyIterator e un numero di elementi pari a 5

Oppure possiamo scrivere i nostri gestori di asserzioni personalizzati.

Expect::that(function ($value) {
	// restituiamo false se l'aspettativa fallisce
});

Esame delle asserzioni errate

Quando un'asserzione fallisce, Tester visualizza dov'è l'errore. Se confrontiamo strutture più complesse, Tester crea dump dei valori confrontati e li salva nella directory output. Ad esempio, in caso di fallimento del test fittizio Arrays.recursive.phpt, i dump verranno salvati come segue:

app/
└── tests/
	├── output/
	│   ├── Arrays.recursive.actual    # valore attuale
	│   └── Arrays.recursive.expected  # valore atteso
	│
	└── Arrays.recursive.phpt          # test fallito

Il nome della directory può essere modificato tramite Tester\Dumper::$dumpDir.