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 дає понад синтаксис: datasets з іменами у виводі, архітектурні тести через `arch()` і вбудоване мутаційне тестування через `--mutate`.