<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
ТЕСТУВАННЯ · JUNIOR

Чим Pest відрізняється від PHPUnit і чи можна їх змішувати?

Pest це надбудова над PHPUnit: з кожного файлу з `it()`/`test()` він генерує клас, що успадковує `TestCase`, і запускає його раннером PHPUnit з тим самим `phpunit.xml`. Тому класові тести працюють поруч без змін, асерти PHPUnit доступні, а Pest додає `expect()`-ланцюжки, datasets, хуки, `arch()`, мутаційне тестування й профілювання.

Pest це окремий тестовий фреймворк чи обгортка над PHPUnit?
У проєкті 800 тестів на PHPUnit. Що доведеться переписати, щоб перейти на Pest?
Звідки в замиканні Pest береться `$this->get()`?
Чому `->with([User::factory()->create()])` падає ще до запуску тесту?
Pest PHPUnit expect datasets tests/Pest.php

Pest не має власного рушія. Кожен файл з викликами it() і test() він перетворює на клас, що успадковує PHPUnit\Framework\TestCase, а замикання стають його методами. Запускає все раннер PHPUnit: у Pest 5 це PHPUnit 13, з тим самим phpunit.xml, тими самими звітами JUnit і покриттям. Звідси головне для співбесіди: класовий тест з методами test_* у тій самій теці Pest запустить без змін, а асерти $this->assertSame() працюють і всередині замикань.

Те, що в PHPUnit жило в базовому класі, у Pest описують у tests/Pest.php. Рядок pest()->extend(Tests\TestCase::class)->use(RefreshDatabase::class)->in('Feature') каже, від якого класу успадковуються тести з теки Feature і які трейти до них додати. Тому в замиканні є $this->get() і $this->actingAs(): $this там справжній екземпляр тест-класу. А в tests/Unit, якого цей рядок не стосується, Laravel не піднімається взагалі.

Понад синтаксис Pest дає кілька речей, яких у чистому PHPUnit немає або вони незручні. expect() з ланцюжком перевірок, де ->and() переводить ланцюжок на нове значення, а ->not заперечує лише наступну перевірку. Datasets трьох видів: масив у ->with(), іменований dataset() і спільні з tests/Datasets, з ключами рядків у виводі. Архітектурні тести через arch(), мутаційне тестування через --mutate, --profile для найповільніших тестів, ->repeat() для пошуку flaky-поведінки, todo() для запланованих тестів. Браузерні тести живуть в окремому плагіні pestphp/pest-plugin-browser.

Найчастіша пастка повʼязана з тим, коли Pest обчислює дані. Масив датасету PHP виконує, щойно Pest завантажує файл і реєструє тести, тобто до setUp(): застосунку й бази ще немає. ->with([User::factory()->create()]) падає з невиразною помилкою, а ->with([fn () => User::factory()->create()]) працює, бо замикання Pest викличе вже всередині тесту. Те саме стосується beforeAll(): у ньому немає $this, бо екземпляр тесту ще не створено.

Вибір між ними здебільшого питання стилю команди. Laravel-проєкти за замовчуванням ідуть з Pest, Symfony частіше лишається на PHPUnit. Перехід не вимагає переписування: старі класи працюють, нові тести пишуться в Pest, а структура хорошого тесту (одна поведінка, arrange, act, assert) від синтаксису не залежить.

// PHPUnit: клас, метод з префіксом test, асерт через $this.
final class EmailTest extends \PHPUnit\Framework\TestCase
{
    #[\PHPUnit\Framework\Attributes\DataProvider('invalid')]
    public function test_it_rejects_an_invalid_address(string $value): void
    {
        $this->expectException(InvalidArgumentException::class);

        new Email($value);
    }

    public static function invalid(): array
    {
        return ['без собачки' => ['olena.example.com'], 'порожній' => ['']];
    }
}

// Pest: той самий тест; під капотом теж клас, що успадковує TestCase.
it('rejects an invalid address', function (string $value) {
    new Email($value);
})->throws(InvalidArgumentException::class)
  ->with(['без собачки' => 'olena.example.com', 'порожній' => '']);

it('normalizes the address', function () {
    $email = new Email('  Olena@Example.COM ');

    expect($email->value)->toBe('olena@example.com')
        ->and($email->domain())->toBe('example.com')
        ->and($email->value)->not->toContain(' ');   // not лише для toContain
});
Що в Pest немає власного рушія: це PHPUnit під капотом, той самий конфіг, звіти й покриття (у Pest 5 це PHPUnit 13).
Що `tests/Pest.php` замінює базовий клас: `pest()->extend(TestCase::class)->use(RefreshDatabase::class)->in('Feature')`, звідси `$this->get()` і трейти в замиканнях.
Що класові тести PHPUnit запускаються Pest без змін, тому міграцію роблять поступово, а не переписуванням.
Ключові можливості: `expect()` з `->and()` і `->not`, datasets (inline, іменовані, спільні в `tests/Datasets`), `beforeEach()`, `arch()`, `--mutate`, `--parallel`, `--profile`.
Що масив датасету обчислюється під час реєстрації тестів, до `setUp()`, тому все, що потребує застосунку чи бази, загортають у замикання.
Казати, що Pest має власний раннер і несумісний з PHPUnit-тестами, тож перехід означає переписати все.
Вважати, що `->not` заперечує весь ланцюжок: `expect($x)->not->toBeNull()->toBe(5)` означає «не null і дорівнює 5», `->not` діє лише на наступну перевірку.
Підключити `RefreshDatabase` у `tests/Pest.php` без `->in('Feature')`: unit-тести домену починають піднімати Laravel і мігрувати базу.
Лишати тести з `->skip()` без причини місяцями: набір зелений, а сценарій ніхто не перевіряє. Для запланованого є `todo()`.
ПОРАДА

Скажіть одним реченням: «Pest це синтаксис і плагіни поверх PHPUnit, а не заміна». Далі покажіть один тест в обох стилях і назвіть три речі, які Pest дає понад синтаксис: datasets з іменами у виводі, архітектурні тести через `arch()` і вбудоване мутаційне тестування через `--mutate`.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Pest це надбудова над PHPUnit з тим самим phpunit.xml, тому міграція поступова. ->not стосується лише наступної перевірки. Масив датасету обчислюється під час реєстрації тестів, коли застосунку й бази ще немає, тому фабрики загортають у замикання.