Asserzioni

Le asserzioni servono a confermare che il valore reale corrisponde a quello atteso. Sono metodi della classe Tester\Assert.

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

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

Gli esempi presuppongono che sia creato questo 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(), cioè 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 ignorano l'identità degli oggetti, l'ordine delle coppie chiave ⇒ valore negli array e i numeri decimali che differiscono in modo trascurabile, il che si può cambiare impostando $matchIdentity e $matchOrder.

Dal punto di vista di equal() questi casi sono uguali, ma per same() no:

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] si può scrivere anche come [0 => 1, 1 => 2], quindi [1 => 2, 0 => 1] verrà considerato uguale.

In $expected potete usare anche le cosiddette Expectations.

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 (confrontato in modo stretto).

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::hasNotKey (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, cioè $value === true.

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

$value deve essere vero, cioè soddisfa la condizione if ($value) ....

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

$value deve essere false, cioè $value === false.

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

$value deve essere falso, cioè soddisfa la condizione if (!$value) ....

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

$value deve essere null, cioè $value === null.

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

$value non deve essere null, cioè $value !== null.

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

$value deve essere Not a Number. Per testare i valori NAN usate esclusivamente Assert::nan(). Il valore NAN è molto particolare e asserzioni come Assert::same() o Assert::equal() possono comportarsi in modo inatteso.

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

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

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

$value deve essere del tipo indicato. 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
  • il nome di una classe oppure direttamente un oggetto, e allora deve valere $value instanceof $type

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

Chiamando $callable deve essere lanciata un'eccezione della classe $class. Se indichiamo $message, anche il messaggio dell'eccezione deve corrispondere al pattern. E se indichiamo $code, anche i codici devono corrispondere in modo stretto.

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

Assert::exception(
	fn() => throw new App\InvalidValueException('Valore zero'),
	App\InvalidValueException::class,
	'Il valore è troppo basso',
);

Assert::exception() restituisce l'eccezione lanciata, così potete testare anche un'eccezione annidata.

$e = Assert::exception(
	fn() => throw new MyException('Qualcosa non va', 0, new RuntimeException),
	MyException::class,
	'Qualcosa non va',
);

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

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

Verifica che la funzione $callable abbia generato gli errori attesi (cioè warning, notice ecc.). Come $type indicate una delle costanti E_..., per esempio E_WARNING. E se indichiamo $message, anche il messaggio di errore deve corrispondere al pattern. Per 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 passate in $type un array:

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)

Verifica che la funzione $callable non abbia generato alcun warning, errore o eccezione. Torna utile per testare pezzi di codice in cui 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 oppure caratteri jolly.

Se come $pattern passiamo un'espressione regolare, dobbiamo delimitarla con ~ oppure #. Altri delimitatori non sono supportati. Per esempio un test in cui $var deve contenere solo cifre esadecimali:

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

La seconda variante assomiglia al confronto tra stringhe normali, ma in $pattern possiamo usare vari caratteri jolly:

  • %a% uno o più caratteri qualsiasi tranne i caratteri di fine riga
  • %a?% zero o più caratteri qualsiasi tranne i caratteri di fine riga
  • %A% uno o più caratteri qualsiasi compresi i caratteri di fine riga
  • %A?% zero o più caratteri qualsiasi compresi i caratteri di fine riga
  • %s% uno o più caratteri di spaziatura tranne i caratteri di fine riga
  • %s?% zero o più caratteri di spaziatura tranne i caratteri di fine riga
  • %S% uno o più caratteri tranne i caratteri di spaziatura
  • %S?% zero o più caratteri tranne i caratteri di spaziatura
  • %c% un singolo carattere qualsiasi (tranne fine riga)
  • %d% una o più cifre
  • %d?% zero o più cifre
  • %i% valore intero con segno
  • %f% numero in virgola mobile
  • %h% una o più cifre esadecimali
  • %w% uno o più caratteri alfanumerici
  • %ds% separatore di directory (/ oppure \)
  • %% un carattere %

Esempi:

# di nuovo il test di un 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::notMatch (string $pattern, $actual, ?string $description=null)

L'opposto di Assert::match().

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

Questa asserzione è identica a Assert::match(), ma il pattern viene caricato dal file $file. Torna utile per testare stringhe molto lunghe. Il file del test resta leggibile.

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

Questa asserzione fallisce sempre. A volte serve. Come opzione possiamo indicare il valore atteso e quello reale.

Expectations

Quando vogliamo confrontare strutture più complesse con elementi non costanti, le asserzioni citate sopra possono non bastare. Per esempio stiamo testando un metodo che crea un nuovo utente e ne restituisce gli attributi come array. Non conosciamo il valore dell'hash della password, ma sappiamo che deve essere una stringa esadecimale. E dell'elemento successivo sappiamo solo che deve essere un oggetto DateTime.

In queste situazioni possiamo usare Tester\Expect dentro il parametro $expected dei metodi Assert::equal() e Assert::notEqual(), con cui la struttura si descrive facilmente.

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 corrisponde 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 che con Assert. Abbiamo quindi a disposizione i metodi Expect::same(), Expect::match(), Expect::count() ecc. Possiamo inoltre concatenarli:

Expect::type(MyIterator::class)->andCount(5);  # ci aspettiamo MyIterator e che il numero di elementi sia 5

In alternativa possiamo scrivere gestori di asserzione nostri.

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

Esaminare le asserzioni fallite

Quando un'asserzione fallisce, Tester stampa in che cosa consiste l'errore. Se confrontiamo strutture complesse, Tester crea dei dump dei valori confrontati e li salva nella directory output. Se per esempio fallisse il test immaginario Arrays.recursive.phpt, i dump verrebbero salvati così:

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

Il nome della directory lo possiamo cambiare con Tester\Dumper::$dumpDir.