Classes utilitaires

HttpAssert

La classe Tester\HttpAssert fournit des outils pour tester des serveurs HTTP. Elle vous permet d'effectuer facilement des requêtes HTTP et de vérifier les codes de statut, les en-têtes et le contenu du corps de la réponse à l'aide d'une interface fluide.

# Requête HTTP de base et vérification de la réponse
$response = Tester\HttpAssert::fetch('https://example.com/api/users');
$response
	->expectCode(200)
	->expectHeader('Content-Type', contains: 'json')
	->expectBody(contains: 'users');

La méthode fetch() crée par défaut une requête GET, mais tous les paramètres peuvent être personnalisés :

HttpAssert::fetch(
	'https://api.example.com/users',
	method: 'POST',
	headers: [
		'Authorization' => 'Bearer token123',  # tableau associatif
		'Accept: application/json',            # ou format chaîne
	],
	cookies: ['session' => 'abc123'],
	follow: false,                             # ne pas suivre les redirections
	body: '{"name": "John"}'
)
	->expectCode(201);

Les codes de statut se vérifient à l'aide des méthodes expectCode() et denyCode(). Vous pouvez passer soit un nombre précis, soit une fonction de validation :

$response
	->expectCode(200)                           # code exact
	->expectCode(fn($code) => $code < 400)      # validation personnalisée
	->denyCode(404)                             # ne doit pas être 404
	->denyCode(fn($code) => $code >= 500);      # ne doit pas être une erreur serveur

Pour vérifier les en-têtes, utilisez les méthodes expectHeader() et denyHeader(). Vous pouvez contrôler qu'un en-tête existe, vérifier sa valeur exacte, ou faire correspondre une partie de son contenu :

$response
	->expectHeader('Content-Type')                     # l'en-tête doit exister
	->expectHeader('Content-Type', 'application/json') # valeur exacte
	->expectHeader('Content-Type', contains: 'json')   # contient le texte
	->expectHeader('Server', matches: 'nginx %a%')     # correspond au motif
	->denyHeader('X-Powered-By')                       # l'en-tête ne doit pas exister
	->denyHeader('X-Debug', contains: 'sensitive')     # ne doit pas contenir le texte
	->denyHeader('X-Debug', matches: '~debug~i');      # ne doit pas correspondre au motif

La vérification du corps de la réponse fonctionne de la même façon avec les méthodes expectBody() et denyBody() :

$response
	->expectBody('OK')                              # valeur exacte
	->expectBody(contains: '"status": "success"')   # contient un fragment JSON
	->expectBody(matches: '%A% hello %A%')          # correspond au motif
	->expectBody(fn($body) => json_decode($body) !== null) # validation personnalisée
	->denyBody('Error occurred')                    # ne doit pas avoir cette valeur exacte
	->denyBody(contains: 'error')                   # ne doit pas contenir le texte
	->denyBody(matches: '~exception|fatal~i');      # ne doit pas correspondre au motif

Le paramètre follow détermine la façon dont HttpAssert traite les redirections HTTP :

# Test d'une redirection sans la suivre (par défaut)
HttpAssert::fetch('https://example.com/redirect', follow: false)
	->expectCode(301)
	->expectHeader('Location', 'https://example.com/new-url');

# Suivi de toutes les redirections jusqu'à la réponse finale
HttpAssert::fetch('https://example.com/redirect', follow: true)
	->expectCode(200)
	->expectBody(contains: 'final content');

DomQuery

Tester\DomQuery est une classe étendant SimpleXMLElement, qui permet d'interroger facilement du HTML ou du XML à l'aide de sélecteurs CSS.

# crée un DomQuery à partir d'une chaîne HTML
$dom = Tester\DomQuery::fromHtml('
	<article class="post">
		<h1>Title</h1>
		<div class="content">Text</div>
	</article>
');

# teste l'existence d'un élément à l'aide de sélecteurs CSS
Assert::true($dom->has('article.post'));
Assert::true($dom->has('h1'));

# trouve les éléments sous forme de tableau d'objets DomQuery
$headings = $dom->find('h1');
Assert::same('Title', (string) $headings[0]);

# teste si l'élément correspond au sélecteur (depuis la version 2.5.3)
$content = $dom->find('.content')[0];
Assert::true($content->matches('div'));
Assert::false($content->matches('p'));

# trouve l'ancêtre le plus proche correspondant au sélecteur (depuis la 2.5.5)
$article = $content->closest('.post');
Assert::true($article->matches('article'));

Pour les documents XML, utilisez la méthode fromXml() :

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

FileMock

Tester\FileMock émule des fichiers en mémoire et facilite le test du code qui utilise des fonctions comme fopen(), file_get_contents(), parse_ini_file() et consorts. Exemple d'utilisation :

# Classe testée
class Logger
{
	public function __construct(
		private string $logFile,
	) {
	}

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

# Nouveau fichier vide
$file = Tester\FileMock::create('');

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

# Teste le contenu créé
Assert::same("Login\nLogout\n", file_get_contents($file));

Le second paramètre facultatif $extension définit l'extension du fichier dans l'URL générée, ce qui est pratique lorsque le code testé prend ses décisions d'après elle :

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

Assert::with()

Ce n'est pas une assertion, mais une aide pour tester les méthodes et propriétés privées des objets.

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

$ent = new Entity;

Assert::with($ent, function () {
	Assert::true($this->enabled); // accès à la propriété privée $ent->enabled
});

Helpers::purge()

La méthode purge() crée le répertoire indiqué et, s'il existe déjà, supprime tout son contenu. Elle est utile pour créer un répertoire temporaire. Par exemple dans tests/bootstrap.php :

@mkdir(__DIR__ . '/tmp');  # @ - le répertoire peut déjà exister

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

Environment::lock()

Les tests s'exécutent en parallèle. Il arrive cependant que nous ayons besoin que leur exécution ne se chevauche pas. C'est typiquement le cas des tests de base de données, qui exigent de préparer le contenu de la base et de garantir qu'aucun autre test n'y touche pendant leur exécution. Dans ces tests, nous utilisons Tester\Environment::lock($name, $dir) :

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

Le premier paramètre est le nom du verrou, le second le chemin du répertoire où le stocker. Le test qui obtient le verrou en premier poursuit, les autres tests doivent attendre qu'il se termine.

Environment::bypassFinals()

Les classes ou méthodes marquées final sont difficiles à tester. Appeler Tester\Environment::bypassFinals() au début d'un test fait omettre les mots-clés final lors du chargement du code.

require __DIR__ . '/bootstrap.php';

Tester\Environment::bypassFinals();

class MyClass extends NormallyFinalClass  # <-- NormallyFinalClass n'est plus final
{
	// ...
}

Environment::setup()

  • améliore la lisibilité des dumps d'erreurs (colorisation comprise) ; sinon, c'est la pile d'appels PHP par défaut qui est affichée
  • active le contrôle que des assertions ont bien été appelées dans le test ; sinon, les tests sans assertions (oubliées, par exemple) passent aussi
  • démarre automatiquement la collecte d'informations sur le code exécuté (quand --coverage est utilisé) (décrit plus loin)
  • affiche le statut OK ou FAILURE à la fin du script

Environment::setupFunctions()

Crée les fonctions globales test(), testException(), testNoError(), setUp() et tearDown(), dans lesquelles vous pouvez structurer vos tests.

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

Environment::VariableRunner

Permet de déterminer si le test a été lancé directement ou via Tester.

if (getenv(Tester\Environment::VariableRunner)) {
	# lancé par Tester
} else {
	# lancé autrement
}

Environment::VariableThread

Tester exécute les tests en parallèle dans le nombre de threads indiqué. Si le numéro du thread nous intéresse, nous le trouvons dans la variable d'environnement :

echo "Exécution dans le thread numéro " . getenv(Tester\Environment::VariableThread);