Tests schreiben
Tests für Nette Tester zu schreiben ist deshalb besonders, weil jeder Test ein PHP-Skript ist, das sich eigenständig ausführen lässt. Darin steckt großes Potenzial. Schon beim Schreiben eines Tests können Sie ihn einfach ausführen und prüfen, ob er richtig funktioniert. Tut er es nicht, können Sie ihn in Ihrer IDE bequem durchsteppen und den Fehler finden.
Sie können den Test sogar im Browser öffnen. Vor allem aber führen Sie mit dem Ausführen den Test aus. Sie erfahren sofort, ob er bestanden oder fehlgeschlagen ist.
Im einleitenden Kapitel haben wir einen sehr einfachen Test mit Array-Operationen gezeigt. Nun erstellen wir eine eigene Klasse zum Testen, auch wenn sie ebenfalls einfach sein wird.
Beginnen wir mit einer typischen Verzeichnisstruktur für eine Bibliothek oder ein Projekt. Wichtig ist, die Tests vom übrigen Code zu trennen, zum Beispiel wegen des Deployments, denn wir wollen die Tests nicht auf den Produktionsserver hochladen. Die Struktur kann so aussehen:
├── src/ # Code, den wir testen werden
│ ├── Rectangle.php
│ └── ...
├── tests/ # Tests
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
Legen wir nun die einzelnen Dateien an. Wir beginnen mit der getesteten Klasse, die wir in der Datei
src/Rectangle.php ablegen:
<?php
class Rectangle
{
private float $width;
private float $height;
public function __construct(float $width, float $height)
{
if ($width < 0 || $height < 0) {
throw new InvalidArgumentException('The dimension must not be negative.');
}
$this->width = $width;
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
public function isSquare(): bool
{
return $this->width === $this->height;
}
}
Und dafür erstellen wir einen Test. Der Dateiname des Tests sollte dem Muster *Test.php oder *.phpt
entsprechen; wir wählen die Variante RectangleTest.php:
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
// allgemeines Rechteck
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # wir prüfen die erwarteten Ergebnisse
Assert::false($rect->isSquare());
Wie Sie sehen, dienen Assertion-Methoden wie Assert::same() dazu, zu bestätigen,
dass ein tatsächlicher Wert einem erwarteten Wert entspricht.
Der letzte Schritt ist die Datei bootstrap.php. Sie enthält den Code, der allen Tests gemeinsam ist, zum Beispiel
das Autoloading der Klassen, die Konfiguration der Umgebung, das Anlegen eines temporären Verzeichnisses, Hilfsfunktionen und
Ähnliches. Alle Tests laden den Bootstrap und widmen sich danach nur noch dem Testen. Der Bootstrap kann so aussehen:
<?php
require __DIR__ . '/vendor/autoload.php'; # lädt den Composer-Autoloader
Tester\Environment::setup(); # Initialisierung von Nette Tester
// und weitere Konfiguration (nur ein Beispiel, in unserem Fall nicht nötig)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
Dieser Bootstrap setzt voraus, dass der Composer-Autoloader auch die Klasse Rectangle.php laden kann.
Das erreichen Sie zum Beispiel, indem Sie den Abschnitt autoload in
composer.json einrichten.
Den Test können wir nun von der Kommandozeile aus ausführen wie jedes andere eigenständige PHP-Skript. Der erste Lauf zeigt uns eventuelle Syntaxfehler, und wenn nirgends ein Tippfehler steckt, wird ausgegeben:
$ php RectangleTest.php
OK
Würden wir im Test die Zusicherung in eine falsche ändern, etwa Assert::same(123, $rect->getArea());,
passiert das hier:
$ php RectangleTest.php Failed: 200.0 should be 123 in RectangleTest.php(5) Assert::same(123, $rect->getArea()); FAILURE
Beim Schreiben von Tests ist es gute Praxis, alle Grenzfälle abzudecken. Zum Beispiel Eingaben wie null, negative Zahlen, in anderen Fällen etwa leere Strings, null usw. Das zwingt Sie geradezu, nachzudenken und zu entscheiden, wie sich der Code in solchen Situationen verhalten soll. Die Tests halten dieses Verhalten dann fest.
In unserem Fall soll ein negativer Wert eine Exception werfen, was wir mit Assert::exception() prüfen:
// die Breite darf nicht negativ sein
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
'The dimension must not be negative.',
);
Und einen entsprechenden Test ergänzen wir für die Höhe. Zum Schluss testen wir, dass isSquare()
true zurückgibt, wenn beide Abmessungen gleich sind. Versuchen Sie, solche Tests als Übung selbst zu schreiben.
Übersichtlichere Tests
Die Testdatei kann wachsen und schnell unübersichtlich werden. Deshalb ist es praktisch, die einzelnen getesteten Bereiche in eigene Funktionen zu gruppieren.
Sehen wir uns zuerst die einfachere und dennoch elegante Möglichkeit mit der globalen Funktion test() an. Tester
legt diese Funktion nicht automatisch an, um Kollisionen zu vermeiden, falls Sie in Ihrem Code eine Funktion mit demselben Namen
haben. Sie wird von der Methode setupFunctions() erzeugt, die Sie in Ihrer Datei bootstrap.php aufrufen
sollten:
Tester\Environment::setup();
Tester\Environment::setupFunctions();
Mit dieser Funktion können wir die Testdatei schön in benannte Einheiten gliedern. Bei der Ausführung werden die Bezeichnungen nacheinander ausgegeben.
<?php
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
test('allgemeines Rechteck', function () {
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
});
test('allgemeines Quadrat', function () {
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
});
test('Abmessungen dürfen nicht negativ sein', function () {
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
);
Assert::exception(
fn() => new Rectangle(10, -1),
InvalidArgumentException::class,
);
});
Wenn Sie vor oder nach jedem test() Code ausführen müssen, übergeben Sie ihn der Funktion setUp()
bzw. tearDown():
setUp(function () {
// Initialisierungscode, der vor jedem test() läuft
});
Die zweite Variante ist objektorientiert. Wir erstellen einen sogenannten TestCase, also eine Klasse, in der die einzelnen
Einheiten Methoden sind, deren Namen mit test beginnen.
class RectangleTest extends Tester\TestCase
{
public function testGeneralOblong()
{
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea());
Assert::false($rect->isSquare());
}
public function testGeneralSquare()
{
$rect = new Rectangle(5, 5);
Assert::same(25.0, $rect->getArea());
Assert::true($rect->isSquare());
}
/** @throws InvalidArgumentException */
public function testWidthMustNotBeNegative()
{
$rect = new Rectangle(-1, 20);
}
/** @throws InvalidArgumentException */
public function testHeightMustNotBeNegative()
{
$rect = new Rectangle(10, -1);
}
}
// Testmethoden ausführen
(new RectangleTest)->run();
Diesmal haben wir die Annotation @throws verwendet, um Exceptions zu testen. Mehr dazu erfahren Sie im Kapitel TestCase.
Hilfsfunktionen
Nette Tester enthält mehrere Klassen und Funktionen, die das Testen erleichtern können, zum Beispiel das Testen des Inhalts von HTML-Dokumenten, das Testen von Funktionen, die mit Dateien arbeiten, und so weiter.
Ihre Beschreibung finden Sie auf der Seite Hilfsklassen.
Annotationen und Überspringen von Tests
Die Ausführung von Tests lässt sich durch Annotationen im phpDoc-Kommentar am Anfang der Datei beeinflussen. Das kann zum Beispiel so aussehen:
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
Die gezeigten Annotationen besagen, dass der Test nur mit PHP ab Version 7.2 und nur dann ausgeführt werden soll, wenn die
PHP-Extensions pdo und pdo_pgsql vorhanden sind. Diese Annotationen interpretiert der Test-Runner auf der Kommandozeile, der den Test überspringt, wenn die Bedingungen nicht erfüllt
sind, und ihn in der Ausgabe mit dem Buchstaben s (skipped) kennzeichnet. Wenn der Test manuell ausgeführt wird,
haben diese Annotationen jedoch keine Wirkung.
Eine Beschreibung der Annotationen finden Sie auf der Seite Test-Annotationen.
Ein Test lässt sich auch aufgrund einer eigenen Bedingung mit Environment::skip() überspringen. So wird der Test
zum Beispiel unter Windows übersprungen:
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Requires UNIX.');
}
Verzeichnisstruktur
Bei Bibliotheken oder Projekten, die auch nur etwas größer sind, empfehlen wir, das Testverzeichnis nach dem Namespace der getesteten Klasse in Unterverzeichnisse aufzuteilen:
└── tests/
├── NamespaceOne/
│ ├── MyClass.getUsers.phpt
│ ├── MyClass.setUsers.phpt
│ └── ...
│
├── NamespaceTwo/
│ ├── MyClass.creating.phpt
│ ├── MyClass.dropping.phpt
│ └── ...
│
├── bootstrap.php
└── ...
So können Sie die Tests eines einzelnen Namespace, also eines Unterverzeichnisses, ausführen:
tester tests/NamespaceOne
Spezielle Situationen
Ein Test, der keine einzige Assertion-Methode aufruft, gilt als verdächtig und wird als Fehler gewertet:
Error: This test forgets to execute an assertion.
Wenn ein Test ohne Assertions absichtlich gültig sein soll, rufen Sie Assert::true(true) auf, um ihn so zu
kennzeichnen.
exit() oder die() zu verwenden, um einen Test mit einer Fehlermeldung zu beenden, kann irreführend
sein. exit('Error in connection') beendet den Test zum Beispiel mit dem Rückgabecode 0, was Erfolg signalisiert.
Verwenden Sie stattdessen Assert::fail('Error in connection').