Tests schreiben
Das Schreiben von Tests für Nette Tester ist einzigartig, da jeder Test ein PHP-Skript ist, das separat ausgeführt werden kann. Dies birgt großes Potenzial. Schon beim Schreiben des Tests können Sie ihn einfach ausführen und feststellen, ob er korrekt funktioniert. Wenn nicht, können Sie ihn leicht in der IDE debuggen und den Fehler suchen.
Sie können den Test sogar im Browser öffnen. Aber vor allem – indem Sie ihn ausführen, führen Sie den Test durch. Sie erfahren sofort, ob er bestanden hat oder fehlgeschlagen ist.
Im Einführungskapitel haben wir einen wirklich trivialen Test zur Arbeit mit einem Array gezeigt. Jetzt erstellen wir unsere eigene Klasse, die wir testen werden, auch wenn sie ebenfalls einfach sein wird.
Beginnen wir mit einer typischen Verzeichnisstruktur für eine Bibliothek oder ein Projekt. Es ist wichtig, die Tests vom restlichen Code zu trennen, beispielsweise wegen des Deployments, da wir Tests nicht auf den Produktionsserver hochladen möchten. Die Struktur könnte etwa so aussehen:
├── src/ # Code, den wir testen werden
│ ├── Rectangle.php
│ └── ...
├── tests/ # Tests
│ ├── bootstrap.php
│ ├── RectangleTest.php
│ └── ...
├── vendor/
└── composer.json
Und nun erstellen wir die einzelnen Dateien. Beginnen wir mit der zu testenden Klasse, die wir in der Datei
src/Rectangle.php
platzieren:
<?php
class Rectangle
{
private float $width;
private float $height;
public function __construct(float $width, float $height)
{
if ($width < 0 || $height < 0) {
throw new InvalidArgumentException('Die Abmessung darf nicht negativ sein.');
}
$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 wir erstellen einen Test dafür. Der Dateiname des Tests sollte dem Muster *Test.php
oder *.phpt
entsprechen, wählen wir beispielsweise 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()); # überprüfen die erwarteten Ergebnisse
Assert::false($rect->isSquare());
Wie Sie sehen, werden sogenannte Assertion-Methoden wie
Assert::same()
verwendet, um zu bestätigen, dass der tatsächliche Wert dem erwarteten Wert entspricht.
Der letzte Schritt ist die Datei bootstrap.php
. Sie enthält Code, der für alle Tests gemeinsam ist, z. B.
Autoloading von Klassen, Umgebungskonfiguration, Erstellung eines temporären Verzeichnisses, Hilfsfunktionen und Ähnliches. Alle
Tests laden den Bootstrap und widmen sich dann nur noch dem Testen. Der Bootstrap kann wie folgt aussehen:
<?php
require __DIR__ . '/vendor/autoload.php'; # lädt den Composer Autoloader
Tester\Environment::setup(); # Initialisierung von Nette Tester
// und weitere Konfigurationen (dies ist nur ein Beispiel, in unserem Fall nicht benötigt)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');
Der angegebene Bootstrap geht davon aus, dass der Composer Autoloader auch die Klasse Rectangle.php
laden kann. Dies kann beispielsweise durch Einstellen
des autoload-Abschnitts in composer.json
usw. erreicht werden.
Den Test können wir nun von der Kommandozeile aus starten, wie jedes andere eigenständige PHP-Skript. Der erste Start deckt eventuelle Syntaxfehler auf, und wenn nirgends ein Tippfehler ist, wird Folgendes ausgegeben:
$ php RectangleTest.php
OK
Wenn wir im Test die Assertion auf einen falschen Wert ändern Assert::same(123, $rect->getArea());
, passiert
Folgendes:
$ 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 gut, alle Grenzfälle abzudecken. Zum Beispiel, wenn die Eingabe Null, eine negative Zahl ist, in anderen Fällen vielleicht eine leere Zeichenkette, null usw. Tatsächlich zwingt es Sie, darüber nachzudenken und zu entscheiden, wie sich der Code in solchen Situationen verhalten soll. Die Tests fixieren dann das Verhalten.
In unserem Fall soll ein negativer Wert eine Ausnahme auslösen, was wir mit Assert::exception() überprüfen:
// Breite darf nicht negativ sein
Assert::exception(
fn() => new Rectangle(-1, 20),
InvalidArgumentException::class,
'Die Abmessung darf nicht negativ sein.',
);
Und einen ähnlichen Test fügen wir für die Höhe hinzu. Schließlich testen wir, ob isSquare()
true
zurückgibt, wenn beide Dimensionen gleich sind. Versuchen Sie als Übung, solche Tests zu schreiben.
Übersichtlichere Tests
Die Größe der Testdatei kann zunehmen und schnell unübersichtlich werden. Daher ist es praktisch, einzelne Testbereiche in separate Funktionen zu gruppieren.
Zuerst zeigen wir eine einfachere, aber elegante Variante, und zwar mithilfe der globalen Funktion test()
. Tester
erstellt sie nicht automatisch, um Kollisionen zu vermeiden, falls Sie eine Funktion mit demselben Namen im Code haben. Sie wird
erst von der Methode setupFunctions()
erstellt, die Sie in der Datei bootstrap.php
aufrufen:
Tester\Environment::setup();
Tester\Environment::setupFunctions();
Mit dieser Funktion können wir die Testdatei schön in benannte Einheiten unterteilen. Beim Ausführen werden die Beschreibungen 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() ausgeführt wird
});
Die zweite Variante ist objektorientiert. Wir erstellen einen sogenannten TestCase, eine Klasse, in der einzelne Einheiten Methoden darstellen, 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);
}
}
// Ausführung der Testmethoden
(new RectangleTest)->run();
Zum Testen von Ausnahmen haben wir diesmal die Annotation @throw
verwendet. Mehr dazu erfahren Sie im Kapitel TestCase.
Hilfsfunktionen
Nette Tester enthält mehrere Klassen und Funktionen, die Ihnen beispielsweise das Testen des Inhalts eines HTML-Dokuments, das Testen von Funktionen, die mit Dateien arbeiten, und so weiter erleichtern können.
Ihre Beschreibung finden Sie auf der Seite Hilfsklassen.
Annotationen und Überspringen von Tests
Die Ausführung von Tests kann durch Annotationen in Form eines phpDoc-Kommentars am Anfang der Datei beeinflusst werden. Sie kann beispielsweise so aussehen:
/**
* @phpExtension pdo, pdo_pgsql
* @phpVersion >= 7.2
*/
Die angegebenen Annotationen besagen, dass der Test nur mit PHP Version 7.2 oder höher und nur dann ausgeführt werden soll,
wenn die PHP-Erweiterungen pdo und pdo_pgsql vorhanden sind. Diese Annotationen werden vom Kommandozeilen-Teststarter berücksichtigt, der den Test überspringt, wenn
die Bedingungen nicht erfüllt sind, und ihn in der Ausgabe mit dem Buchstaben s
– skipped – markiert. Bei
manueller Ausführung des Tests haben sie jedoch keine Auswirkung.
Die Beschreibung der Annotationen finden Sie auf der Seite Test-Annotationen.
Ein Test kann auch basierend auf der Erfüllung einer eigenen Bedingung mit Environment::skip()
übersprungen
werden. Zum Beispiel überspringen wir Tests unter Windows:
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
Tester\Environment::skip('Requires UNIX.');
}
Verzeichnisstruktur
Wir empfehlen, bei etwas größeren Bibliotheken oder Projekten das Testverzeichnis noch in Unterverzeichnisse nach dem Namespace der getesteten Klasse aufzuteilen:
└── tests/
├── NamespaceOne/
│ ├── MyClass.getUsers.phpt
│ ├── MyClass.setUsers.phpt
│ └── ...
│
├── NamespaceTwo/
│ ├── MyClass.creating.phpt
│ ├── MyClass.dropping.phpt
│ └── ...
│
├── bootstrap.php
└── ...
Sie können dann Tests aus einem einzigen Namespace bzw. Unterverzeichnis ausführen:
tester tests/NamespaceOne
Spezielle Situationen
Ein Test, der keine einzige Assertionsmethode aufruft, ist verdächtig und wird als fehlerhaft bewertet:
Error: This test forgets to execute an assertion.
Wenn ein Test wirklich ohne Aufruf von Assertions als gültig betrachtet werden soll, rufen Sie beispielsweise
Assert::true(true)
auf.
Es kann auch trügerisch sein, exit()
und die()
zu verwenden, um einen Test mit einer Fehlermeldung
zu beenden. Zum Beispiel beendet exit('Error in connection')
den Test mit dem Rückgabecode 0, was Erfolg
signalisiert. Verwenden Sie Assert::fail('Error in connection')
.