Testo: niezależny framework do testów jednostkowych i benchmarków w PHP
Nowy framework Testo uruchamia fazę beta. To samodzielna implementacja bez zależności od PHPUnit, z obsługą PHP 8.2+, wtyczkami, wbudowanymi testami oraz benchmarkami. Zaprojektowany dla programistów średnio- i starszego szczebla — skupia się na elastyczności, pełnej typizacji i minimalnym boilerplate’ie.
Główne różnice względem PHPUnit i innych rozwiązań
Testo powstał jako odpowiedź na ograniczenia PHPUnit: brak elastyczności, częste zmiany łamiące kompatybilność oraz bardzo ograniczony rozwój poza wsparciem nowych wersji PHP. Autor uczestniczył w dyskusjach i przesyłał pull requesty, ale propozycje nie zostały zaakceptowane. PEST i Codeception dziedziczą te same problemy, a dodatkowo mają nieintuicyjny składniowy zapis.
Testo jest całkowicie niezależny:
- Nie korzysta z biblioteki
nikic/php-parser, eliminując ryzyko konfliktów. - Wspiera PHP 8.2 i nowsze wersje.
- Agenci AI generują testy automatycznie na podstawie pliku
llms.txtw dokumentacji.
System wtyczek umożliwia pełną personalizację pod potrzeby projektu. Każda funkcjonalność to opcjonalna wtyczka, a każdy zestaw testów (suite) może mieć własny zestaw wtyczek. Przykładem jest wtyczka #[Retry]: wystarczy dodać dwa pliki, by włączyć ponowne uruchamianie testów po porażce.
Rozszerzanie funkcjonalności odbywa się poprzez potoki (pipelines), warstwy pośrednie (middleware) i zdarzenia. Testy jednostkowe można umieszczać bezpośrednio w kodzie źródłowym dzięki atrybutowi #[TestInline], a logikę testową definiować za pomocą atrybutów.
Instalacja i konfiguracja
Instalacja przez Composer:
composer require --dev testo/testo
Utwórz plik testo.php w katalogu głównym projektu:
<?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'],
),
],
);
Plik zwraca instancję ApplicationConfig. Jeśli go nie ma, Testo używa domyślnej konfiguracji z katalogu tests/. Dwa zestawy testów: Sources — dla wbudowanych testów i benchmarków w kodzie źródłowym, Tests — dla tradycyjnych testów jednostkowych.
Uruchomienie: ./vendor/bin/testo lub przez wtyczkę PHPStorm.
Przykłady testów jednostkowych
Klasa nie dziedziczy po żadnej bazowej klasie testowej — wystarczy oznaczyć metody atrybutem #[Test]:
final class OrderTest
{
#[Test]
public function calculatesTotal(): void
{
$order = new Order();
$order->addItem('Book', price: 15.0, quantity: 2);
$order->addItem('Pen', price: 3.0, quantity: 5);
Assert::same($order->total(), 45.0);
}
#[Test]
#[DataSet([100.0, 10, 90.0], '10% off')]
#[DataSet([100.0, 0, 100.0], 'no discount')]
#[DataSet([0.0, 50, 0.0], 'zero price')]
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);
}
}
Atrybut #[Test] na klasie oznacza, że wszystkie metody void lub never są traktowane jako testy. Funkcje spoza klas nie wymagają atrybutów cyklu życia.
W metodach Assert pierwszym argumentem jest wartość rzeczywista, drugim — oczekiwana. Obsługa łańcuchów asercji:
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');
Wbudowane testy i benchmarki
Atrybut #[TestInline] w katalogu src pozwala testować prywatne metody bezpośrednio w miejscu ich definicji:
// 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);
}
}
Benchmarki z atrybutem #[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;
}
Wynik wykonania:
+---+----------+-------+---------+------------------+--------+
| # | Name | Iters | Calls | Avg Time | RStDev |
+---+----------+-------+---------+------------------+--------+
| 2 | current | 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% |
+---+----------+-------+---------+------------------+--------+
Kluczowe zalety
- Niezależny framework bez żadnych zależności od PHPUnit — pełna kontrola nad architekturą i możliwość dostosowania za pomocą wtyczek.
- Wbudowane testy (
#[TestInline]) i benchmarki (#[Bench]) bezpośrednio w kodzie źródłowym — nie trzeba tworzyć osobnych katalogów ani plików. - Typowane łańcuchy asercji i deklaratywne atrybuty — zero boilerplate’u i maksymalna czytelność.
- Konfiguracja w czystym PHP — przejrzysta obsługa wielu zestawów testów (suites).
- Pełna integracja z PHPStorm oraz wygodne uruchamianie z CLI.
— Editorial Team
Brak komentarzy.