Skip to content
...
Блог
Beta-тестирование открыто!

Beta-тестирование открыто!

Testo — это тестовый фреймворк для PHP, написанный с нуля. Не обёртка, не форк, не надстройка, а полностью независимая архитектура на плагинах, мидлварях и событиях.

Получился гибкий инструмент с приятным API и возможностями, выходящими за рамки привычного тестирования — от встроенных тестов прямо в src до бенчмарков одним атрибутом.

Зачем ещё один тестовый фреймворк?

Существующие решения построены на архитектуре, которая складывалась годами и несёт в себе много легаси. Затащить туда что-то принципиально новое крайне сложно. Testo начат с чистого листа — это дало свободу выбирать правильные решения, не оглядываясь на обратную совместимость.

Какова философия Testo?

Дать разработчику полный контроль, ничего не навязывая.
Testo не диктует, как писать тесты. Он предоставляет инструменты и механизмы расширения, а вы решаете, как их использовать. Всё, что не нужно, можно отключить. Всё, чего не хватает, можно добавить.

Testo пропитан уважением.

  • PHP 8.2+ — самая широкая поддержка версий среди тестовых фреймворков. Если ваш проект не может прыгнуть на свежую версию PHP, это не повод сидеть без обновлений и фиксов безопасности.
  • Выход новой версии PHP не означает, что нужно делать новый мажорный релиз фреймворка.
  • Никакой политики Zero Issues, ведь Testo хочет решать проблемы, а не заметать их под ковёр.
Чем Testo отличается от PHPUnit и Pest?

Расширяемость. Встроенные тесты, бенчмарки, retry — всё это обычные плагины, построенные на тех же механизмах, что доступны и вам. Написать свой плагин — пара десятков строк кода. Создать свой атрибут с крутой логикой? Проще простого, #[Retry]#[Retry(int $maxAttempts = 3, bool $markFlaky = true)]Объявляет политику повторного запуска теста при падении. — отличный пример.

Гибкость. Из Testo можно слепить именно то, что нужно вам. Всё, что не нужно, можно отключить. Каждый Test Suite может иметь индивидуальный набор плагинов.

Можно ли использовать Testo вместе с другими тестовыми фреймворками?

Да. Testo полностью независим и не конфликтует с другими инструментами — он даже не патчит nikic/php-parser и не использует его. Можно поставить Testo в проект рядом с Codeception, PHPUnit или Pest и начать писать на нём новые тесты, не трогая существующие.

Подходит ли Testo для разработки с AI-агентами?

Да. Testo предоставляет llms.txt и llms-full.txt — специальные файлы для AI-агентов с описанием API фреймворка. Скормите их вашему агенту, и он сразу начнёт писать тесты на Testo. Подробнее — в документации.

Есть ли поддержка IDE?

Для PHPStorm есть полноценный плагин — запуск тестов, навигация и отображение результатов прямо в IDE.

Насколько Testo готов к использованию?

Сейчас идёт бета-тестирование. Пользовательский API стабилизировался, документация пишется, плагин для PHPStorm работает. До релиза осталось допилить отчёты, параллельный запуск и несколько мелочей.

Сложно ли перейти с PHPUnit?

Testo использует привычный синтаксис PHP и ООП: никакого DSL, магических методов или необычных конструкций. Если вы пишете на PHP, вы уже знаете синтаксис Testo. Это пока не drop-in замена PHPUnit, но порог входа минимальный.

Отдельно стоит сказать про проверки. В семействе xUnit (PHPUnit, JUnit, NUnit) исторически сложился порядок аргументов $expected, $actual, источник бесконечной путаницы.

  • webmozart/assert решил это просто: поставил $actual первым.
  • PEST выбрал fluent API как в Jest: expect($actual)->not()->toBe($expected), но ценой магического DSL, нетипичного для PHP.
  • Testo взял лучшее из обоих подходов: прямой порядок аргументов и типизированные цепочки, которые попутно разгрузили основной фасад. Например: Assert::string($email)->contains('@'). Никакой магии, полная поддержка IDE и статического анализа.

Установка и настройка

Всего 3 шага:

  1. Установите Testo через Composer:

    bash
    composer require --dev testo/testo
  2. Создайте testo.php в корне проекта:

    php
    <?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'],
            ),
        ],
    );
    Что это за файл?

    Testo конфигурируется PHP-файлом, который возвращает объект ApplicationConfig\Testo\Application\Config\ApplicationConfig. Если файла нет, Testo попытается запустить тесты из папки tests с дефолтными настройками.

    Здесь мы определили два набора тестов:

    • Sources для встроенных тестов и бенчмарков прямо в коде проекта, в папке src;
    • Tests для привычных Unit-тестов в папке tests.
  3. Установите плагин для PHPStorm:

    PSTesto for PhpStorm JetBrains Marketplace 4.1276 downloads

Запускать тесты можно прямо из PHPStorm, с помощью плагина, или через CLI:

bash
./vendor/bin/testo

Первые тесты

Unit-тест

Создаём обычный класс с методами, помеченными атрибутом #[Test]#[Test()]Явно помечает метод, функцию или класс как тест.. Никакого наследования от базовых классов:

php
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);
    }
}

А вот как выглядят типизированные цепочки проверок:

php
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');

Expect::exception(PaymentException::class)
    ->fromMethod(PaymentGateway::class, 'charge')
    ->withMessageContaining('insufficient funds')
    ->withCode(402);

Встроенные тесты

Тестируйте методы прямо там, где они объявлены. Отдельный тестовый файл не нужен — атрибут #[TestInline]#[TestInline(array $arguments, mixed $result = null)]Объявляет встроенный тест на методе или функции. запускает метод с заданными аргументами и проверяет результат. Работает даже с приватными методами:

php
// 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);
    }
}

Идеально для чистых функций и быстрого прототипирования — тест живёт рядом с кодом и обновляется вместе с ним.

Бенчмарки

Моментально сравнивайте производительность функций и методов не отвлекаясь на обвязку: достаточно повесить атрибут #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями. на функцию и бенчи уже полетели:

php
#[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;
}
Results for viaDivision:
+----------------------------+--------------------------------------------+---------+
| BENCHMARK SETUP            | TIME RESULTS                               | SUMMARY |
| Name     | Iters | Calls   | Mean            | Median          | RStDev | Place   |
+----------+-------+---------+-----------------+-----------------+--------+---------+
| current  | 10    | 2000000 | 76.04ns         | 75.88ns         | ±1.14% | 2nd     |
| multiply | 10    | 2000000 | 78.60ns (+3.4%) | 78.79ns (+3.8%) | ±1.18% | 3rd     |
| shift    | 10    | 2000000 | 70.60ns (-7.2%) | 70.66ns (-6.9%) | ±1.70% | 1st     |
+----------+-------+---------+-----------------+-----------------+--------+---------+

Интересно?

Если вас заинтересовал Testo, но вы хотите узнать о нём чуть больше, обязательно ознакомьтесь с этими статьями:

Поставьте звёздочку на GitHub и оценку PHPStorm плагину — это очень поможет Testo стать более заметным.

Что дальше?

Сейчас идёт бета-тестирование и мы двигаемся к релизу. Пользовательский API стабилизировался, но есть ещё несколько вещей, которые нужно доделать:

  • Причесать вывод отчётов в CLI и PHPStorm, добавить diff.
  • Всякие мелочи, вроде перехвата STDOUT и PHP-ошибок.
  • Параллельный запуск тестов и изолированный запуск в отдельном процессе.
  • Допилить незначительные вещи в бенчах и internal, чтобы было совсем хорошо.
  • Всякие организационные моменты, вроде "раскидать монорепу" и дописать документацию.

Codecov и Mocks, возможно, тоже подъедут к релизу, но это уже не точно.

Вы же, в свою очередь, можете помочь с тестированием и фидбеком, чтобы релиз был максимально гладким и безболезненным. Приходите в GitHub Issues или Telegram-чат с идеями, вопросами и проблемами — будем разбираться вместе!