Hilfsklassen
HttpAssert
Die Klasse Tester\HttpAssert stellt Werkzeuge zum Testen von HTTP-Servern bereit. Sie erlaubt es, auf einfache
Weise HTTP-Requests auszuführen und Statuscodes, Header und den Inhalt des Response-Bodys über ein Fluent Interface zu
prüfen.
# Einfacher HTTP-Request und Prüfung der Response
$response = Tester\HttpAssert::fetch('https://example.com/api/users');
$response
->expectCode(200)
->expectHeader('Content-Type', contains: 'json')
->expectBody(contains: 'users');
Die Methode fetch() erzeugt standardmäßig einen GET-Request, aber alle Parameter lassen sich anpassen:
HttpAssert::fetch(
'https://api.example.com/users',
method: 'POST',
headers: [
'Authorization' => 'Bearer token123', # assoziatives Array
'Accept: application/json', # oder String-Format
],
cookies: ['session' => 'abc123'],
follow: false, # Weiterleitungen nicht folgen
body: '{"name": "John"}'
)
->expectCode(201);
Statuscodes lassen sich mit den Methoden expectCode() und denyCode() prüfen. Sie können entweder
eine konkrete Zahl oder eine Prüffunktion übergeben:
$response
->expectCode(200) # exakter Code
->expectCode(fn($code) => $code < 400) # eigene Prüfung
->denyCode(404) # darf nicht 404 sein
->denyCode(fn($code) => $code >= 500); # darf kein Serverfehler sein
Zur Prüfung von Headern dienen die Methoden expectHeader() und denyHeader(). Sie können prüfen, ob
ein Header existiert, seinen exakten Wert prüfen oder einen Teil seines Inhalts abgleichen:
$response
->expectHeader('Content-Type') # Header muss existieren
->expectHeader('Content-Type', 'application/json') # exakter Wert
->expectHeader('Content-Type', contains: 'json') # enthält Text
->expectHeader('Server', matches: 'nginx %a%') # passt zum Muster
->denyHeader('X-Powered-By') # Header darf nicht existieren
->denyHeader('X-Debug', contains: 'sensitive') # darf den Text nicht enthalten
->denyHeader('X-Debug', matches: '~debug~i'); # darf nicht zum Muster passen
Die Prüfung des Response-Bodys funktioniert ähnlich mit den Methoden expectBody() und
denyBody():
$response
->expectBody('OK') # exakter Wert
->expectBody(contains: '"status": "success"') # enthält ein JSON-Fragment
->expectBody(matches: '%A% hello %A%') # passt zum Muster
->expectBody(fn($body) => json_decode($body) !== null) # eigene Prüfung
->denyBody('Error occurred') # darf nicht diesen exakten Wert haben
->denyBody(contains: 'error') # darf den Text nicht enthalten
->denyBody(matches: '~exception|fatal~i'); # darf nicht zum Muster passen
Der Parameter follow steuert, wie HttpAssert mit HTTP-Weiterleitungen umgeht:
# Test einer Weiterleitung, ohne ihr zu folgen (Standard)
HttpAssert::fetch('https://example.com/redirect', follow: false)
->expectCode(301)
->expectHeader('Location', 'https://example.com/new-url');
# Allen Weiterleitungen bis zur endgültigen Response folgen
HttpAssert::fetch('https://example.com/redirect', follow: true)
->expectCode(200)
->expectBody(contains: 'final content');
DomQuery
Tester\DomQuery ist eine Klasse, die SimpleXMLElement erweitert und das bequeme Abfragen von HTML
oder XML mit CSS-Selektoren ermöglicht.
# DomQuery aus einem HTML-String erzeugen
$dom = Tester\DomQuery::fromHtml('
<article class="post">
<h1>Title</h1>
<div class="content">Text</div>
</article>
');
# Existenz von Elementen mit CSS-Selektoren prüfen
Assert::true($dom->has('article.post'));
Assert::true($dom->has('h1'));
# Elemente als Array von DomQuery-Objekten finden
$headings = $dom->find('h1');
Assert::same('Title', (string) $headings[0]);
# prüfen, ob ein Element zum Selektor passt (seit Version 2.5.3)
$content = $dom->find('.content')[0];
Assert::true($content->matches('div'));
Assert::false($content->matches('p'));
# den nächsten Vorfahren finden, der zum Selektor passt (seit 2.5.5)
$article = $content->closest('.post');
Assert::true($article->matches('article'));
Für XML-Dokumente verwenden Sie die Methode fromXml():
$dom = Tester\DomQuery::fromXml('<catalog><item>First</item></catalog>');
Assert::true($dom->has('item'));
FileMock
Tester\FileMock emuliert Dateien im Speicher und erleichtert das Testen von Code, der Funktionen wie
fopen(), file_get_contents(), parse_ini_file() und ähnliche verwendet.
Anwendungsbeispiel:
# Getestete Klasse
class Logger
{
public function __construct(
private string $logFile,
) {
}
public function log(string $message): void
{
file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
}
}
# Neue leere Datei
$file = Tester\FileMock::create('');
$logger = new Logger($file);
$logger->log('Login');
$logger->log('Logout');
# Den erzeugten Inhalt testen
Assert::same("Login\nLogout\n", file_get_contents($file));
Der optionale zweite Parameter $extension setzt die Dateiendung in der erzeugten URL, was praktisch ist, wenn der
getestete Code anhand davon entscheidet:
$file = Tester\FileMock::create('{"key": "value"}', 'json');
Assert::with()
Das ist keine Assertion, sondern ein Helfer zum Testen privater Methoden und Properties von Objekten.
class Entity
{
private $enabled;
// ...
}
$ent = new Entity;
Assert::with($ent, function () {
Assert::true($this->enabled); // das private $ent->enabled ist zugänglich
});
Helpers::purge()
Die Methode purge() legt das angegebene Verzeichnis an, und wenn es bereits existiert, löscht sie dessen gesamten
Inhalt. Sie ist zum Anlegen eines temporären Verzeichnisses nützlich. Zum Beispiel in tests/bootstrap.php:
@mkdir(__DIR__ . '/tmp'); # @ - das Verzeichnis kann bereits existieren
define('TempDir', __DIR__ . '/tmp/' . getmypid());
Tester\Helpers::purge(TempDir);
Environment::lock()
Tests laufen parallel. Manchmal brauchen wir aber, dass sich die Ausführung der Tests nicht überschneidet. Typischerweise
erfordern Datenbanktests, den Inhalt der Datenbank vorzubereiten und sicherzustellen, dass während der Ausführung kein anderer
Test in die Datenbank eingreift. In diesen Tests verwenden wir Tester\Environment::lock($name, $dir):
Tester\Environment::lock('database', __DIR__ . '/tmp');
Der erste Parameter ist der Name der Sperre, der zweite der Pfad zum Verzeichnis, in dem die Sperre gespeichert wird. Der Test, der die Sperre zuerst erhält, läuft weiter, die übrigen Tests müssen auf seinen Abschluss warten.
Environment::bypassFinals()
Als final markierte Klassen oder Methoden lassen sich schwer testen. Ein Aufruf von
Tester\Environment::bypassFinals() am Anfang eines Tests bewirkt, dass die Schlüsselwörter final beim
Laden des Codes weggelassen werden.
require __DIR__ . '/bootstrap.php';
Tester\Environment::bypassFinals();
class MyClass extends NormallyFinalClass # <-- NormallyFinalClass ist nicht mehr final
{
// ...
}
Environment::setup()
- verbessert die Lesbarkeit der Fehler-Dumps (einschließlich Färbung); sonst wird der Standard-Stacktrace von PHP ausgegeben
- aktiviert die Prüfung, ob im Test Assertions aufgerufen wurden; sonst gelten auch Tests ohne Assertions (z. B. vergessene) als bestanden
- startet automatisch das Sammeln von Informationen über den ausgeführten Code (wenn
--coverageverwendet wird) (weiter unten beschrieben) - gibt am Ende des Skripts den Status OK oder FAILURE aus
Environment::setupFunctions()
Erzeugt die globalen Funktionen test(), testException(), testNoError(),
setUp() und tearDown(), mit denen Sie Ihre Tests gliedern können.
test('Beschreibung des Tests', function () {
Assert::same(123, foo());
Assert::false(bar());
// ...
});
Environment::VariableRunner
Erlaubt es festzustellen, ob der Test direkt oder über den Tester ausgeführt wurde.
if (getenv(Tester\Environment::VariableRunner)) {
# vom Tester ausgeführt
} else {
# auf andere Weise ausgeführt
}
Environment::VariableThread
Tester führt die Tests parallel in der angegebenen Anzahl von Threads aus. Wenn uns die Nummer des Threads interessiert, ermitteln wir sie aus der Umgebungsvariablen:
echo "Läuft in Thread Nummer " . getenv(Tester\Environment::VariableThread);