Новый фреймворк тестирования Testo готов к испытаниям.
Website: https://php-testo.github.io/ru/
Полгода назад я начал разработку Testo по причине того, что PHPUnit меня не просто не устраивал, а сильно мешал:
не давал нужной гибкости и расширяемости;
вызывал много проблем на дистанции, ломая совместимость каждые два года;
задеревенел и, крмое повышения требований к PHP, никак не развивался.
В обычном случае это можно было бы поправить каким-то вкладом в репозиторий, но у PHPUnit свой путь. Я отслеживал и участвовал в Issues на гитхабе, но всё, что я там видел, выглядело примерно так:

Собственно, это и стало отправной точкой для Testo.
Автор статьи «PHPUnit: рабочий нерабочий covers» тоже на это наткнулся.
Фрагмент
Но главная сложность - это упорное нежелание автора библиотеки добавлять этот флаг. Хотя в phpunit уже есть схожие решения (--disable-coverage-ignore, --no-coverage).
Я честно пробовал провернуть этот финт. Вот даже PR-ы в оба репозитория:
К сожалению, спустя какое-то (весьма продолжительное) время все они были закрыты Себастьяном Бергманном с излюбленной формулировкой:
Thank you for your contribution. I appreciate the time you invested in preparing this pull request. However, I have decided not to merge it.
Примерный перевод: Мне настолько плевать на этот PR, что я закрою его даже не объясняя причины.
От жеж ..! Ладно, бывает.
Но постойте, есть же ещё PEST и Codeception.
Эти фреймворки хоть и предоставляют много дополнительных фичей, но всё-же тоже базируются на PHPUnit и наследуют проблемные гены родителя. Кроме того, нетрадиционный синтаксис PEST из Jest лично меня отталкивает.

Что предлагает Testo
Testo уживается с любыми библиотеками и инструментами, не вызывая проблем.
Не зависит от PHPUnit. Это не очередная обертка над ним, а полноценный фреймворк с нуля.
Не патчит
nikic/php-parserи даже его не использует. Конфликтов не будет.PHP 8.2+ — самая широкая поддержка версий PHP.
AI-агенты легко сгенерируют Testo-тесты, просто скормите им llms.txt (дока).
Благодаря системе плагинов вы можете слепить из Testo именно то, что нужно вам.
Ограничений, кроме иммутабельности, нет.
Все фичи Testo — это плагины, которые можно включать и отключать по желанию.
Написать свой плагин? Пара десятков строк кода, и он уже в деле.
Каждый Test Suite может иметь индивидуальный набор плагинов.
Выходите за рамки привычного тестирования:
Надо тестировать прямо в
src? Для этого уже есть встроенные тесты и бенчмарки.Сделать свой атрибут с крутой логикой? Проще простого, плагин
#[Retry], состоящий из двух файлов — отличный пример [1][2].Система пайплайнов и мидлварей, система событий и плагины дают безграничные возможности для расширения и кастомизации.
Сделано разработчиком для разработчиков.
Минимум боилерплейта благодаря атрибутам.
Типизация даже в проверках.
Привычный синтаксис ООП и PHP, без магии и DSL.
Полноценный плагин для PHPStorm тоже имеется.
Ну что, попробуем?
Установка и настройка
Всего 3 шага:
Установите Testo через Composer:
composer require --dev testo/testoСоздайте
testo.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 попытается запустить тесты из папкиtestsс дефолтными настройками.Здесь мы определили два набора тестов:
-Sourcesдля встроенных тестов и бенчмарков прямо в папкеsrc;
-Testsдля привычных Unit-тестов в папкеtests.Установите плагин для PHPStorm
Запускать тесты можно прямо из PHPStorm или через CLI:
./vendor/bin/testo
Первые тесты
Unit-тест
В папке tests/ создаём обычный класс с методами, помеченными атрибутом #[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); } }
Дополнительно
Атрибут
#[Test]можно повесить на класс, тогда все методы с возвращаемым типомvoidилиneverавтоматически будут считаться тестами.Тесты можно описывать вне класса, просто в функциях. Это ещё меньше кода, но атрибуты жизненного цикл будут недоступны
Методы фасада Assert принимают интуитивно понятный прямой порядок аргументов: сначала $actual (проверяемое значение), затем $expected (ожидаемое значение). Это отличается от устаревшего подхода xUnit.
А вот как выглядят цепочки типизированных проверок:
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');
Встроенные тесты
Тестируйте методы прямо там, где они объявлены. Отдельный тестовый файл не нужен — атрибут #[TestInline] запускает метод с заданными аргументами и проверяет результат. Работает даже с приватными методами:
// 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( 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; }
+---+----------+-------+---------+------------------+--------+ | # | 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% | +---+----------+-------+---------+------------------+--------+
Интересно?
Если вас заинтересовал Testo, но вы хотите узнать о нём чуть больше, обязательно ознакомьтесь с этими статьями:
«К коллайдеру!» — про бенчмарки и сравнение производительности.
«Assert и Expect» — про новый и старый API для проверок и ожиданий.
«Data Providers» — про провайдеры данных для тестов.
Поставьте звёздочку на GitHub и оценку PHPStorm плагину — это очень поможет Testo стать более заметным.
Что дальше?
Сейчас идёт бета-тестирование и мы двигаемся к релизу. Пользовательский API стабилизировался, однако есть ещё несколько вещей, которые нужно доделать:
Причесать вывод отчётов в CLI и PHPStorm, добавить diff.
Всякие мелочи, вроде перехвата STDOUT и PHP-ошибок.
Параллельный запуск тестов и изолированный запуск в отдельном процессе.
Допилить незначительные вещи в бенчах и internal, чтобы было совсем хорошо.
Всякие организационные моменты, вроде "раскидать монорепу" и дописать документацию.
Codecov и Mocks, возможно, тоже подъедут к релизу, но это уже не точно.
Вы же, в свою очередь, можете помочь с тестированием и фидбеком, чтобы релиз был максимально гладким и безболезненным. Приходите в GitHub Issues или Telegram-чат с идеями, вопросами и проблемами — будем разбираться вместе!
