Skip to content
...

Пишем тесты

Testo не диктует, как и где писать тесты. Отдельные тесты в классах и функциях, встроенные тесты прямо на продакшн-коде, бенчмарки — все подходы можно комбинировать в одном проекте.

Подходы к написанию тестов

ПодходОбнаружениеКогда использовать
Отдельные тесты#[Test]#[Test()]Явно помечает метод, функцию или класс как тест. / конвенцииUnit, feature, integration
Встроенные тесты#[TestInline]#[TestInline(array $arguments, mixed $result = null)]Объявляет встроенный тест на методе или функции.Простые проверки в коде приложения
Бенчмарки#[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Объявляет бенчмарк для сравнения производительности метода с альтернативными реализациями.Сравнение производительности

Отдельные тесты

Чаще всего тесты пишутся в классах и функциях, отдельно от тестируемого кода, в директории tests/.

Хороший тест следует паттерну AAA — Arrange, Act, Assert:

php
function calculatesOrderTotal(): void
{
    // Arrange — подготовка
    $order = new Order();
    $order->addItem('Book', price: 15.0, quantity: 2);
    $order->addItem('Pen', price: 3.0, quantity: 5);

    // Act — действие
    $total = $order->total();

    // Assert — проверка
    Assert::same($total, 45.0);
}
php
function throwsOnNegativeAmount(): never
{
    // Arrange
    $account = new Account(balance: 100);

    // Assert — до действия
    Expect::exception(InsufficientFundsException::class);

    // Act
    $account->withdraw(200);
}
php
// Для простых тестов AAA избыточен
function defaultCurrencyIsUsd(): void
{
    Assert::same(new Money(100)->currency, 'USD');
}

Для проверок Testo предоставляет два фасада из плагина Assert:

  • Assert\Testo\Assert — утверждения, проверяются здесь и сейчас. Поддерживает цепочки типизированных проверок.
  • Expect\Testo\Expect — ожидания, проверяются после завершения теста (исключения, утечки памяти).
php
// Assert — утверждения
Assert::same($user->name, 'John');
Assert::true($user->isActive);
Assert::string($email)->contains('@');

// Assert — цепочка типизированных проверок
Assert::string($response->body)
    ->contains('success')
    ->notContains('error');

// Expect — ожидания поведения теста
Expect::exception(\RuntimeException::class);
Expect::notLeaks($connection);

Атрибуты

Вместо базовых классов или магических методов Testo делает ставку на атрибуты.

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

Конвенции именования

Плагин Convention находит тесты по паттернам именования — атрибуты не нужны. По умолчанию это суффикс *Test на классе и префикс test* на методах:

php
// tests/Unit/OrderTest.php
final class OrderTest
{
    public function testCreatesOrder(): void { /* ... */ }

    public function testCalculatesTotal(): void { /* ... */ }

    public function testAppliesDiscount(): void { /* ... */ }
}

Convention не входит в набор плагинов по умолчанию — при необходимости его нужно подключить.

Практические советы

  • Называйте тесты как сценарииcalculatesDiscountForVipCustomer понятнее, чем testDiscount. При падении имя теста — первое, что вы увидите.
  • Один тест — один сценарий. Несколько проверок в тесте — нормально, но несколько сценариев — нет. Если тест проверяет и создание, и удаление — разделите их.
  • Придерживайтесь AAA (Arrange, Act, Assert). При этом комментарии // Arrange // Act // Assert не обязательны: достаточно разделять блоки пустой строкой.

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

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

php
#[TestInline([1, 1], 2)]
#[TestInline([40, 2], 42)]
#[TestInline([-5, 5], 0)]
public static function sum(int $a, int $b): int
{
    return $a + $b;
}

Каждый атрибут запускает метод с заданными аргументами и проверяет результат. Работает даже с приватными методами.

Подходит для простых чистых функций и быстрого прототипирования.

Бенчмарки

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

php
#[Bench(
    callables: [
        'array' => [self::class, 'sumInArray'],
    ],
    arguments: [1, 5_000],
    calls: 2000,
    iterations: 10,
)]
public static function sumInCycle(int $a, int $b): int
{
    $result = 0;
    for ($i = $a; $i <= $b; ++$i) {
        $result += $i;
    }
    return $result;
}

Testo прогоняет функции заданное число раз, фильтрует выбросы и выдаёт статистику с рекомендациями. Пошаговый разбор — в статье «К коллайдеру!».

Структура папок

Рекомендуемая структура, подходящая для большинства приложений:

project/
├── src/                  ← inline-тесты, бенчмарки
│   └── ...
└── tests/
    ├── Unit/
    │   └── ...
    ├── Feature/
    │   └── ...
    └── Integration/
        └── ...

Каждый Suite — это не только отдельная папка, но и отдельный SuiteConfig с нужным набором плагинов. Например:

  • Unit — быстрые изолированные тесты, можно запускать параллельно.
  • Feature — требуют контейнер приложения, HTTP-клиент, базу данных.
  • Integration — работают с реальными внешними сервисами, последовательный запуск.
  • Sources — inline-тесты и бенчмарки в коде приложения.
php
return new ApplicationConfig(
    suites: [
        new SuiteConfig(name: 'Unit', location: ['tests/Unit'], plugins: [/* ... */]),
        new SuiteConfig(name: 'Feature', location: ['tests/Feature'], plugins: [/* ... */]),
        new SuiteConfig(name: 'Integration', location: ['tests/Integration'], plugins: [/* ... */]),
        new SuiteConfig(name: 'Sources', location: ['src'], plugins: [/* ... */]),
    ],
);

В модульной архитектуре тесты могут жить в модуле, а конфиги — собираться в один, как в монорепозитории.