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