Powrót do strony głównej

Testo framework: testy i benchmarki PHP

Testo — niezależny framework do unit testów i benchmarków w PHP 8.2+. Obsługuje wtyczki, wbudowane testy w src, typizowane Assert. Instalacja przez Composer, przykłady kodu dla deweloperów middle/senior.

Testo: nowy framework testów dla PHP 8.2+
Advertisement 728x90

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:

Google AdInline article slot
  • 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.txt w 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:

Google AdInline article slot
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.

Google AdInline article slot

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

Advertisement 728x90

Czytaj dalej