Tests zum Schreiben

Das Schreiben von Tests für Nette Tester ist einzigartig, da jeder Test ein PHP-Skript ist, das eigenständig ausgeführt werden kann. Das hat großes Potenzial. Während Sie den Test schreiben, können Sie ihn einfach ausführen, um zu sehen, ob er richtig funktioniert. Wenn nicht, können Sie ihn in der IDE einfach durchgehen und nach einem Fehler suchen.

Sie können den Test sogar in einem Browser öffnen. Vor allem aber führen Sie den Test aus, indem Sie ihn ausführen. Sie werden sofort erfahren, ob er bestanden hat oder nicht.

Im Einführungskapitel haben wir einen wirklich trivialen Test zur Verwendung eines PHP-Arrays gezeigt. Jetzt werden wir unsere eigene Klasse erstellen, die wir testen werden, obwohl sie auch einfach sein wird.

Beginnen wir mit einem typischen Verzeichnislayout für eine Bibliothek oder ein Projekt. Es ist wichtig, die Tests vom Rest des Codes zu trennen, z. B. aus Gründen der Bereitstellung, da wir die Tests nicht auf den Server hochladen wollen. Die Struktur könnte wie folgt aussehen:

├── src/           # code that we will test
│   ├── Rectangle.php
│   └── ...
├── tests/         # tests
│   ├── bootstrap.php
│   ├── RectangleTest.php
│   └── ...
├── vendor/
└── composer.json

Und nun werden wir einzelne Dateien erstellen. Wir beginnen mit der getesteten Klasse, die wir in der Datei src/Rectangle.php

<?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 wir erstellen einen Test für sie. Der Name der Testdatei sollte mit der Maske *Test.php oder *.phpt übereinstimmen, wir werden die Variante RectangleTest.php wählen:

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

// allgemeines Rechteck
$rect = new Rectangle(10, 20);
Assert::same(200.0, $rect->getArea()); # wir werden die erwarteten Ergebnisse überprüfen
Assert::false($rect->isSquare());

Wie Sie sehen, werden Assertion-Methoden wie Assert::same() verwendet, um sicherzustellen, dass ein tatsächlicher Wert mit einem erwarteten Wert übereinstimmt.

Der letzte Schritt besteht darin, die Datei bootstrap.php zu erstellen. Sie enthält einen gemeinsamen Code für alle Tests. Zum Beispiel das automatische Laden von Klassen, die Umgebungskonfiguration, die Erstellung temporärer Verzeichnisse, Hilfsprogramme und ähnliches. Jeder Test lädt den Bootstrap und kümmert sich nur um den Test. Der Bootstrap kann wie folgt aussehen:

<?php
require __DIR__ . '/vendor/autoload.php'; # Composer Autoloader laden

Tester\Environment::setup(); # Initialisierung des Nette-Testers

// und andere Konfigurationen (nur ein Beispiel, in unserem Fall werden sie nicht benötigt)
date_default_timezone_set('Europe/Prague');
define('TmpDir', '/tmp/app-tests');

Dieser Bootstrap geht davon aus, dass der Composer Autoloader auch die Klasse Rectangle.php laden kann. Dies kann z.B. durch das Setzen der Autoload-Sektion in composer.json erreicht werden, usw.

Wir können den Test nun wie jedes andere eigenständige PHP-Skript von der Kommandozeile aus ausführen. Beim ersten Durchlauf werden alle Syntaxfehler aufgedeckt, und wenn Sie keinen Tippfehler gemacht haben, werden Sie sehen:

$ php RectangleTest.php

OK

Wenn wir im Test die Anweisung in false Assert::same(123, $rect->getArea()); ändern, wird dies geschehen:

$ 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 Extremsituationen abzufangen. Zum Beispiel, wenn die Eingabe Null ist, eine negative Zahl, in anderen Fällen eine leere Zeichenkette, null, usw. Das zwingt Sie dazu, darüber nachzudenken und zu entscheiden, wie sich der Code in solchen Situationen verhalten soll. Die Tests korrigieren dann das Verhalten.

In unserem Fall sollte ein negativer Wert eine Ausnahme auslösen, die wir mit Assert::exception() überprüfen:

// die Breite darf keine negative Zahl sein
Assert::exception(
	fn() => new Rectangle(-1, 20),
	InvalidArgumentException::class,
	'Die Dimension darf nicht negativ sein.',
);

Und wir fügen einen ähnlichen Test für die Höhe hinzu. Schließlich testen wir, dass isSquare() true zurückgibt, wenn beide Dimensionen gleich sind. Versuchen Sie, solche Tests als Übung zu schreiben.

Gut angeordnete Tests

Die Größe der Testdatei kann zunehmen und schnell unübersichtlich werden. Daher ist es sinnvoll, einzelne Testbereiche in separaten Funktionen zu gruppieren.

Zunächst zeigen wir eine einfachere, aber elegante Variante, die die globale Funktion “test()” verwendet. Der Tester erstellt sie nicht automatisch, um eine Kollision zu vermeiden, wenn Sie eine Funktion mit demselben Namen in Ihrem Code haben. Sie wird nur durch die 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. Bei der Ausführung werden die Bezeichnungen nacheinander angezeigt.

<?php
use Tester\Assert;

require __DIR__ . '/bootstrap.php';

test('general oblong', function () {
	$rect = new Rectangle(10, 20);
	Assert::same(200.0, $rect->getArea());
	Assert::false($rect->isSquare());
});

test('general square', function () {
	$rect = new Rectangle(5, 5);
	Assert::same(25.0, $rect->getArea());
	Assert::true($rect->isSquare());
});

test('dimensions must not be negative', function () {
	Assert::exception(
		fn() => new Rectangle(-1, 20),
        InvalidArgumentException::class,
	);

	Assert::exception(
		fn() => new Rectangle(10, -1),
        InvalidArgumentException::class,
	);
});

Wenn Sie den Code vor oder nach jedem Test ausführen müssen, übergeben Sie ihn an setUp() oder tearDown():

setUp(function () {
	// Initialisierungscode, der vor jedem test() auszuführen ist
});

Die zweite Variante ist das Objekt. Wir werden den so genannten TestCase erstellen, eine Klasse, in der einzelne Einheiten durch Methoden dargestellt werden, 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();

Dieses Mal haben wir eine Anmerkung @throw verwendet, um auf Ausnahmen zu testen. Weitere Informationen finden Sie im Kapitel TestCase.

Helfer-Funktionen

Nette Tester enthält mehrere Klassen und Funktionen, die Ihnen das Testen erleichtern können, z.B. Helfer zum Testen des Inhalts eines HTML-Dokuments, zum Testen der Funktionen der Arbeit mit Dateien usw.

Eine Beschreibung dieser Funktionen finden Sie auf der Seite Helpers.

Kommentierung und Überspringen von Tests

Die Testausführung kann durch Anmerkungen im phpDoc-Kommentar am Anfang der Datei beeinflusst werden. Er könnte zum Beispiel so aussehen:

/**
 * @phpExtension pdo, pdo_pgsql
 * @phpVersion >= 7.2
 */

Die Anmerkungen besagen, dass der Test nur mit PHP Version 7.2 oder höher ausgeführt werden sollte und wenn die PHP-Erweiterungen pdo und pdo_pgsql vorhanden sind. Diese Anmerkungen werden vom Befehlszeilen-Testrunner kontrolliert, der, wenn die Bedingungen nicht erfüllt sind, den Test überspringt und ihn mit dem Buchstaben “s” markiert – übersprungen. Sie haben jedoch keine Auswirkungen, wenn der Test manuell ausgeführt wird.

Eine Beschreibung der Annotationen finden Sie unter Testannotationen.

Der Test kann auch auf der Grundlage einer eigenen Bedingung mit Environment::skip() übersprungen werden. Zum Beispiel wird dieser Test unter Windows übersprungen:

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
	Tester\Environment::skip('Requires UNIX.');
}

Verzeichnisstruktur

Bei nur wenig größeren Bibliotheken oder Projekten empfiehlt es sich, das Testverzeichnis in Unterverzeichnisse entsprechend dem Namensraum 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

Edge Cases

Ein Test, der keine Assertion-Methode aufruft, ist verdächtig und wird als fehlerhaft bewertet:

Error: This test forgets to execute an assertion.

Wenn der Test ohne Aufruf von Assertions wirklich als gültig angesehen werden soll, rufen Sie zum Beispiel Assert::true(true) auf.

Es kann auch tückisch sein, exit() und die() zu verwenden, um den Test mit einer Fehlermeldung zu beenden. Zum Beispiel beendet exit('Error in connection') den Test mit dem Exit-Code 0, was einen Erfolg signalisiert. Verwenden Sie Assert::fail('Error in connection').