Testo: Ein eigenständiges PHP-Test- und Benchmarking-Framework
Das neue Testo-Framework steht nun für Beta-Tests zur Verfügung. Es handelt sich um eine abhängigkeitsfreie Implementierung – ohne Abhängigkeit von PHPUnit – mit Unterstützung für PHP 8.2+, Plugins, Inline-Tests und Benchmarks. Speziell für mittlere bis erfahrene PHP-Entwickler konzipiert, legt Testo Wert auf Flexibilität, strikte Typisierung und minimale Boilerplate.
Wichtige Unterschiede zu PHPUnit und Alternativen
Testo wurde entwickelt, um zentrale Schwächen von PHPUnit zu beheben: mangelnde Flexibilität, häufige Breaking Changes und Stillstand jenseits der grundlegenden PHP-Version-Unterstützung. Der Autor war im Issue-Tracker und bei Pull Requests von PHPUnit aktiv beteiligt – doch vorgeschlagene Verbesserungen wurden nicht übernommen. PEST und Codeception erben die Einschränkungen von PHPUnit und fügen zudem umständliche Syntax hinzu.
Testo ist vollständig unabhängig:
- Vermeidet
nikic/php-parserund eliminiert damit parserbezogene Konflikte. - Setzt PHP 8.2+ voraus.
- KI-Agenten können automatisch Tests anhand von
llms.txtgenerieren – einer strukturierten Spezifikation, die aus der offiziellen Dokumentation abgeleitet ist.
Ein modulares Plugin-System ermöglicht projektspezifische Anpassungen. Jedes Feature wird als optionales Plugin ausgeliefert; jede Test-Suite wählt ihre eigene Kombination aus. So fügt das #[Retry]-Plugin beispielsweise Resilienz gegen flaky Tests hinzu – realisiert mit nur zwei schlanken Dateien.
Erweitern Sie das Verhalten über Pipelines, Middleware und Events. Schreiben Sie Inline-Tests direkt in Ihrem src/-Code mit #[TestInline] und nutzen Sie Attribute, um Testlogik sauber einzubetten.
Installation und Konfiguration
Installieren Sie Testo über Composer:
composer require --dev testo/testo
Erstellen Sie testo.php im Projektstammverzeichnis:
<?php
declare(strict_types=1);
use Testo/Application/Config/ApplicationConfig;
use Testo/Application/Config/SuiteConfig;
return new ApplicationConfig(
suites: [
new SuiteConfig(
name: 'Sources',
location: ['src'],
),
new SuiteConfig(
name: 'Tests',
location: ['tests'],
),
],
);
Diese Datei gibt eine Instanz von ApplicationConfig zurück. Ohne sie greift Testo auf Standardwerte zurück (z. B. Scan des tests/-Verzeichnisses). Zwei Standard-Suites sind vordefiniert: Sources für Inline-Tests und Benchmarks innerhalb des Produktionscodes sowie Tests für klassische Unit-Tests.
Führen Sie Tests mit ./vendor/bin/testo aus – oder nutzen Sie das offizielle PHPStorm-Plugin für nahtlose IDE-Integration.
Beispiele für Unit-Tests
Keine Vererbung nötig. Markieren Sie Methoden einfach mit #[Test]:
final class OrderTest
{
#[Test]
public function calculatesTotal(): void
{
$order = new Order();
$order->addItem('Buch', price: 15.0, quantity: 2);
$order->addItem('Stift', price: 3.0, quantity: 5);
Assert::same($order->total(), 45.0);
}
#[Test]
#[DataSet([100.0, 10, 90.0], '10 % Rabatt')]
#[DataSet([100.0, 0, 100.0], 'kein Rabatt')]
#[DataSet([0.0, 50, 0.0], 'Nullpreis')]
public function appliesDiscount(float $price, int $percent, float $expected): void
{
$result = Order::applyDiscount($price, $percent);
Assert::same($result, $expected);
}
#[Test]
#[ExpectException(InsufficientFundsException::class)]
public function cannotOverdraw(): never
{
new Account(balance: 100)->withdraw(200);
}
}
Mit #[Test] am Klassennamen werden alle Methoden mit Rückgabetyp void oder never automatisch als Tests behandelt. Eigenständige Funktionen (außerhalb von Klassen) laufen ohne Lifecycle-Attribute.
Assertions folgen einer intuitiven Reihenfolge: tatsächlicher Wert zuerst, erwarteter Wert zweitens. Kettenbare, typsichere Assertions:
Assert::string($email)->contains('@');
Assert::int($age)->greaterThan(0)->lessThan(150);
Assert::array($items)
->hasKeys('id', 'name')
->isList()
->notEmpty();
Assert::json($response->body())
->isObject()
->hasKeys('data', 'meta');
Inline-Tests und Benchmarks
Nutzen Sie #[TestInline] innerhalb von src/, um private oder interne Methoden zu prüfen:
// src/Money.php
final class Money
{
#[TestInline(['price' => 100.0, 'discount' => 0.1, 'tax' => 0.2], 108.0)]
#[TestInline(['price' => 50.0, 'discount' => 0.0, 'tax' => 0.1], 55.0)]
private static function calculateFinalPrice(
float $price,
float $discount,
float $tax,
): float {
return $price * (1 - $discount) * (1 + $tax);
}
}
Messen Sie Performance mit #[Bench]:
#[Bench(
callables: [
'multiply' => 'viaMultiply',
'shift' => 'viaShift',
],
arguments: [1, 5_000],
calls: 2_000_000,
)]
function viaDivision(int $a, int $b): int
{
$d = $b - $a + 1;
return (int) (($d - 1) * $d / 2) + $a * $d;
}
function viaMultiply(int $a, int $b): int
{
$d = $b - $a + 1;
return (int) (($d - 1) * $d * 0.5) + $a * $d;
}
function viaShift(int $a, int $b): int
{
$d = $b - $a + 1;
return ((($d - 1) * $d) >> 1) + $a * $d;
}
Beispielausgabe:
+---+----------+-------+---------+------------------+--------+
| # | Name | Aufrufe | Calls | Ø Zeit | RStDev |
+---+----------+-------+---------+------------------+--------+
| 2 | aktuell | 10 | 2000000 | 75,890 µs | ±0,79% |
| 3 | multiply | 10 | 2000000 | 78,821 µs (+3,9%)| ±0,47% |
| 1 | shift | 10 | 2000000 | 70,559 µs (−7,0%)| ±0,70% |
+---+----------+-------+---------+------------------+--------+
Warum Testo relevant ist
- Ein wirklich eigenständiges Framework – keine PHPUnit-Abhängigkeiten, erweiterbar über komponierbare Plugins.
- Inline-Tests (
#[TestInline]) und Benchmarks (#[Bench]) leben direkt neben dem Produktionscode insrc/. - Typsichere, fluide Assertion-Ketten – keine Boilerplate, keine Vererbung, keine magischen Globals.
- Konfiguration über reine PHP-Dateien mit vollem Support für Multi-Suite-Projekte.
- Erstklassiges PHPStorm-Plugin und CLI-Tooling – bereit für den täglichen Einsatz.
— Editorial Team
Noch keine Kommentare.