Helper

HttpAssert

La classe Tester\HttpAssert offre strumenti per testare i server HTTP. Permette di eseguire facilmente richieste HTTP e di verificare codici di stato, header e contenuto del corpo della risposta con un'interfaccia fluent.

# richiesta HTTP di base e verifica della risposta
$response = Tester\HttpAssert::fetch('https://example.com/api/users');
$response
	->expectCode(200)
	->expectHeader('Content-Type', contains: 'json')
	->expectBody(contains: 'users');

Il metodo fetch() crea per impostazione predefinita una richiesta GET, ma tutti i parametri si possono personalizzare:

HttpAssert::fetch(
	'https://api.example.com/users',
	method: 'POST',
	headers: [
		'Authorization' => 'Bearer token123',  # array associativo
		'Accept: application/json',            # oppure formato stringa
	],
	cookies: ['session' => 'abc123'],
	follow: false,                             # non seguire i redirect
	body: '{"name": "John"}'
)
	->expectCode(201);

I codici di stato si verificano con i metodi expectCode() e denyCode(). Potete passare un numero preciso oppure una funzione di validazione:

$response
	->expectCode(200)                           # codice esatto
	->expectCode(fn($code) => $code < 400)      # validazione personalizzata
	->denyCode(404)                             # non deve essere 404
	->denyCode(fn($code) => $code >= 500);      # non deve essere un errore del server

Per verificare gli header servono i metodi expectHeader() e denyHeader(). Potete controllare se un header esiste, verificarne il valore esatto oppure una parte del contenuto:

$response
	->expectHeader('Content-Type')                     # l'header deve esistere
	->expectHeader('Content-Type', 'application/json') # valore esatto
	->expectHeader('Content-Type', contains: 'json')   # contiene il testo
	->expectHeader('Server', matches: 'nginx %a%')     # corrisponde al pattern
	->denyHeader('X-Powered-By')                       # l'header non deve esistere
	->denyHeader('X-Debug', contains: 'sensitive')     # non deve contenere il testo
	->denyHeader('X-Debug', matches: '~debug~i');      # non deve corrispondere al pattern

La verifica del corpo della risposta funziona in modo analogo con i metodi expectBody() e denyBody():

$response
	->expectBody('OK')                              # valore esatto
	->expectBody(contains: '"status": "success"')   # contiene un frammento JSON
	->expectBody(matches: '%A% hello %A%')          # corrisponde al pattern
	->expectBody(fn($body) => json_decode($body) !== null) # validazione personalizzata
	->denyBody('Error occurred')                    # non deve avere questo valore esatto
	->denyBody(contains: 'error')                   # non deve contenere il testo
	->denyBody(matches: '~exception|fatal~i');      # non deve corrispondere al pattern

Il parametro follow governa come HttpAssert tratta i redirect HTTP:

# test del redirect senza seguirlo (predefinito)
HttpAssert::fetch('https://example.com/redirect', follow: false)
	->expectCode(301)
	->expectHeader('Location', 'https://example.com/new-url');

# seguendo tutti i redirect fino alla risposta finale
HttpAssert::fetch('https://example.com/redirect', follow: true)
	->expectCode(200)
	->expectBody(contains: 'final content');

DomQuery

Tester\DomQuery è una classe che estende SimpleXMLElement con una comoda interrogazione di HTML o XML tramite selettori CSS.

# creiamo DomQuery da una stringa HTML
$dom = Tester\DomQuery::fromHtml('
	<article class="post">
		<h1>Titolo</h1>
		<div class="content">Testo</div>
	</article>
');

# verifichiamo l'esistenza degli elementi con i selettori CSS
Assert::true($dom->has('article.post'));
Assert::true($dom->has('h1'));

# troviamo gli elementi come array di oggetti DomQuery
$headings = $dom->find('h1');
Assert::same('Titolo', (string) $headings[0]);

# verifichiamo se l'elemento corrisponde al selettore (dalla versione 2.5.3)
$content = $dom->find('.content')[0];
Assert::true($content->matches('div'));
Assert::false($content->matches('p'));

# troviamo l'antenato più vicino che corrisponde al selettore (dalla 2.5.5)
$article = $content->closest('.post');
Assert::true($article->matches('article'));

Per i documenti XML usate il metodo fromXml():

$dom = Tester\DomQuery::fromXml('<catalog><item>First</item></catalog>');
Assert::true($dom->has('item'));

FileMock

Tester\FileMock emula i file in memoria e facilita il test del codice che usa funzioni come fopen(), file_get_contents(), parse_ini_file() e simili. Esempio d'uso:

# classe testata
class Logger
{
	public function __construct(
		private string $logFile,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
	}
}

# nuovo file vuoto
$file = Tester\FileMock::create('');

$logger = new Logger($file);
$logger->log('Login');
$logger->log('Logout');

# verifichiamo il contenuto creato
Assert::same("Login\nLogout\n", file_get_contents($file));

Il secondo parametro opzionale $extension imposta l'estensione del file nell'URL generato, il che è comodo quando il codice testato decide in base a essa:

$file = Tester\FileMock::create('{"key": "value"}', 'json');

Assert::with()

Non è un'asserzione, ma un aiuto per testare metodi e proprietà privati degli oggetti.

class Entity
{
	private $enabled;
	// ...
}

$ent = new Entity;

Assert::with($ent, function () {
	Assert::true($this->enabled); // la privata $ent->enabled è accessibile
});

Helpers::purge()

Il metodo purge() crea la directory indicata e, se esiste già, ne cancella tutto il contenuto. Torna utile per creare una directory temporanea. Per esempio in tests/bootstrap.php:

@mkdir(__DIR__ . '/tmp');  # @ - la directory può esistere già

define('TempDir', __DIR__ . '/tmp/' . getmypid());
Tester\Helpers::purge(TempDir);

Environment::lock()

I test girano in parallelo. A volte però abbiamo bisogno che le esecuzioni dei test non si sovrappongano. Tipicamente i test sul database richiedono di preparare il contenuto del database e di garantire che durante la loro esecuzione nessun altro test interferisca con il database. In questi test usiamo Tester\Environment::lock($name, $dir):

Tester\Environment::lock('database', __DIR__ . '/tmp');

Il primo parametro è il nome del lock, il secondo il percorso della directory in cui salvarlo. Il test che ottiene per primo il lock prosegue, gli altri test devono aspettare che finisca.

Environment::bypassFinals()

Le classi o i metodi contrassegnati come final sono difficili da testare. Chiamare Tester\Environment::bypassFinals() all'inizio del test fa sì che durante il caricamento del codice le parole chiave final vengano omesse.

require __DIR__ . '/bootstrap.php';

Tester\Environment::bypassFinals();

class MyClass extends NormallyFinalClass  # <-- NormallyFinalClass non è più final
{
	// ...
}

Environment::setup()

  • migliora la leggibilità dei dump degli errori (compresa la colorazione); altrimenti viene stampato il consueto stack trace di PHP
  • attiva il controllo che nel test siano state chiamate delle asserzioni; altrimenti passano anche i test senza asserzioni (per esempio dimenticate)
  • avvia automaticamente la raccolta di informazioni sul codice eseguito (quando si usa --coverage) (descritto più avanti)
  • stampa alla fine dello script lo stato OK oppure FAILURE

Environment::setupFunctions()

Crea le funzioni globali test(), testException(), testNoError(), setUp() e tearDown(), con le quali potete dare una struttura ai vostri test.

test('descrizione del test', function () {
	Assert::same(123, foo());
	Assert::false(bar());
	// ...
});

Environment::VariableRunner

Permette di capire se il test è stato eseguito direttamente oppure tramite Tester.

if (getenv(Tester\Environment::VariableRunner)) {
	# eseguito da Tester
} else {
	# eseguito in altro modo
}

Environment::VariableThread

Tester esegue i test in parallelo nel numero di thread indicato. Se ci interessa il numero del thread, lo ricaviamo dalla variabile d'ambiente:

echo "In esecuzione nel thread numero " . getenv(Tester\Environment::VariableThread);