PHPUnit, яким зручно писати
Pest не має власного рушія. Кожен файл з викликами it() і test() він перетворює на клас, що успадковує , і віддає його : у цьому проєкті Pest 5 працює на PHPUnit 13. Тому конфіг лишається в phpunit.xml, а звичайний клас з методами test_* запуститься поруч з тестами Pest без жодної зміни. Переходити можна по одному файлу.
Перевірка починається з expect($value), за яким іде ланцюжок : toBe(), toHaveCount(), toThrow(). Метод ->and($other) переводить ланцюжок на нове значення, а ->not заперечує лише наступну перевірку. Так кілька повʼязаних перевірок читаються одним реченням.
Коли той самий тест треба прогнати на десятку входів, його не копіюють, а дають йому : масив прямо в ->with(), іменований dataset() у тому самому файлі або спільний з теки tests/Datasets, який бачать усі тести. Спільну підготовку виносять у beforeEach() і afterEach(), а describe() збирає тести одного класу в групу зі своїми хуками.
Те, що в PHPUnit писали в базовому класі, у Pest живе у tests/Pest.php: pest()->extend(TestCase::class) каже, від якого класу успадковуються тести з теки, ->use() додає трейти на кшталт RefreshDatabase, а ->in('Feature') обмежує це однією текою. Звідси ж беруться $this->get() і $this->actingAs() у feature-тестах Laravel.
Крім unit- і feature-тестів, Pest уміє ще два види. перевіряє не поведінку, а залежності коду: arch()->expect('App\Domain')->not->toUse('Illuminate'). Браузерні тести живуть в окремому плагіні pestphp/pest-plugin-browser на Playwright: visit('/jobs')->click('Відгукнутися') відкриває справжній браузер. А з прапорців варто знати чотири: --parallel ділить тести між процесами, --profile показує десять найповільніших, --mutate вмикає мутаційне тестування для класів, названих у covers() чи mutates(), а --dirty запускає лише тести з незакоміченими змінами.
Один тест, шість записів
SalaryParser з цього сайту дістає зарплату з назви вакансії: «PHP Developer, 3000-4500$» дає від 3000 до 4500 доларів. Перемикайте вкладки: сценарій той самий, міняється лише те, як його записано. Усі шість варіантів перевірено в Pest 5 на цьому проєкті.
Маленький Pest своїми руками
-
1
рядки 5–20
expect() повертає обʼєкт, а ->not це магічна властивість через __get(): вона лише ставить прапорець для наступної перевірки.
-
2
рядки 32–36
->and() не перевіряє нічого, а повертає новий обʼєкт для іншого значення. Тому ланцюжок і читається як одне речення.
-
3
рядки 38–47
Перевірка кидає виняток, а прапорець not після неї скидається. У Pest так само: ->not діє лише на один крок.
-
4
рядки 55–67
Датасет це цикл: тіло тесту викликається з кожним рядком як з аргументами, а ключ рядка йде в назву.
-
5
рядки 78–88
Третій рядок падає: наївний парсер не розуміє «3.5k». Справжній SalaryParser цей випадок знає, а датасет одразу показує, який саме вхід зламався.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
tests/Pest.php і датасет для SalaryParser
Увесь наш tests/Pest.php займає кілька рядків: feature-тести успадковують Tests\TestCase і отримують RefreshDatabase, а unit-тести лишаються голими й не чіпають бази. Нижче тест SalaryParser з тим самим прийомом, що й у вкладці Dataset: тринадцять назв вакансій, записаних так, як їх пишуть на джоб-бордах, проганяють один тест, і кожен рядок у виводі звітує окремо.
Де Pest підставляє ногу
Модель прямо в масиві датасету
->with([User::factory()->create()]) падає ще до запуску, і то з невиразною помилкою DatasetMissing. Вираз у масиві PHP виконує, щойно читає файл тестів, а застосунку й бази в цей момент ще немає.
Натомість: загорніть значення в замикання: ->with([fn () => User::factory()->create()]). Pest викличе його вже всередині тесту, після setUp() і RefreshDatabase.
Думати, що ->not діє на весь ланцюжок
expect($ids)->not->toBeEmpty()->toContain(42) не означає «не порожній і не містить 42». ->not стосується лише першої перевірки, друга знову звичайна.
Натомість: ставте ->not перед кожною перевіркою, яку заперечуєте, а довгий ланцюжок з кількома ->not краще розбийте на окремі expect().
RefreshDatabase для всіх тестів
pest()->extend(TestCase::class)->use(RefreshDatabase::class) без ->in() вмикає Laravel і міграції навіть для unit-тестів домену. Вони стають помітно повільнішими, а залежність від бази ховається.
Натомість: обмежте це текою: ->in('Feature'), як у tests/Pest.php цього сайту. Unit-тести тоді не піднімають фреймворк і йдуть за мілісекунди.
skip() назавжди
Тест вимкнули через ->skip(), щоб злити гілку, і він місяцями висить жовтим. Набір зелений, а сценарій ніхто не перевіряє.
Натомість: для запланованого пишіть todo('...'), а для вимкненого вказуйте причину: ->skip('чекає на API Нової пошти'). vendor/bin/pest --todos покаже весь список.
- TestCase
- Базовий клас PHPUnit, від якого успадковується кожен тест. Pest генерує такий клас для кожного файлу через eval(), а ваші замикання стають його методами.
- PHPUnit
- Стандартний тестовий фреймворк PHP Себастіана Бергмана (Sebastian Bergmann). Pest використовує його раннер, конфіг phpunit.xml, асерти й звіти про покриття.
- Expectation
- Обʼєкт, який повертає expect(). Кожен його метод перевіряє значення й повертає той самий обʼєкт, тож перевірки йдуть ланцюжком.
- Dataset
- Набір вхідних даних для одного тесту. Pest запускає тест окремо для кожного рядка й показує ключ рядка у виводі.
- arch()
- Архітектурний тест Pest. Він не запускає код, а читає, які класи й функції використовує простір імен, і падає, якщо домен тягне фреймворк чи хтось лишив dd().
- Хук
- Функція, яку Pest викликає навколо тестів: beforeEach() перед кожним тестом файлу чи групи, afterEach() після нього, beforeAll() і afterAll() раз на файл.
Питання зі співбесід
->and($value) не перевіряє нічого сам, а переводить ланцюжок на нове значення, щоб наступні перевірки стосувалися його. ->not заперечує лише одну наступну перевірку: expect($x)->not->toBeNull()->toBe(5) означає «не null і дорівнює 5».
Pest це надбудова: з кожного файлу з it() він генерує клас, що успадковує TestCase, і запускає його раннером PHPUnit з конфігом phpunit.xml. Тому асерти PHPUnit працюють і в Pest, а клас з методами test_* у тій самій теці запускається без змін. На Pest можна переходити по одному файлу.
Масив датасету PHP обчислює, коли Pest завантажує файл і реєструє тести, тобто до setUp(): застосунок ще не піднято, а транзакції RefreshDatabase немає. Замикання Pest викликає вже всередині тесту, тож модель створюється у свіжій базі й відкочується разом з тестом.
Тести починають іти в кількох процесах одночасно, і все спільне стає гонкою: одна база, ті самі файли, кеш, порти, статичні змінні, порядок тестів. Laravel частину цього бере на себе: для кожного процесу створює окрему тестову базу з суфіксом _test_N і окрему теку для Storage::fake(). Решта на вас: тест не має залежати від іншого тесту чи від порядку запуску. А --profile покаже десять найповільніших тестів, з яких варто почати пришвидшення.