Усі вони test doubles, обʼєкти, що в тесті замінюють справжню залежність. Dummy лише заповнює параметр, stub повертає заготовлені відповіді, spy ще й записує виклики для перевірки після дії, mock отримує очікування до дії й сам падає, якщо їх не виконано, fake це робоча спрощена реалізація, як репозиторій на масиві. Stub і fake ведуть до перевірки стану, spy і mock до перевірки поведінки.
Питання на співбесіду з тестування PHP
Питання про тестування PHP-застосунків: види тестів і їхня пропорція, моки, стаби й фейки, Pest і PHPUnit, час у тестах, архітектурні тести, PHPStan і мутаційне тестування.
Вид тесту визначає, скільки справжнього коду бере участь у перевірці: unit бере клас у памʼяті, integration додає одну справжню залежність (базу, кеш, файли), feature проганяє HTTP-запит крізь увесь застосунок, e2e клікає в браузері. Піраміда радить багато unit-тестів і мало повільних, трофей ставить у центр інтеграційні; пропорцію обирають за тим, де в проєкті живуть баги.
Flaky-тест то проходить, то падає на тому самому коді. Найчастіші причини: прихований час (`now()`, `new DateTimeImmutable()`), випадкові дані, залежність від порядку тестів і спільного стану, мережа й паралельність. Час роблять явною залежністю (порт Clock, PSR-20, `FrozenClock`) або зупиняють через `travelTo()`/`freezeTime()`, випадковість передають як `Random\Randomizer` з seed, а порядок перевіряють запуском з `--order-by=random`.
Підміняють те, що повільне, недетерміноване або має побічні ефекти: базу, мережу, пошту, час, випадковість. Value objects і доменні класи беруть справжні. Чужі бібліотеки не мокають напряму: їх загортають у власний вузький порт, домен тестують з фейком цього порту, а сам адаптер integration-тестом проти справжньої бібліотеки чи записаних відповідей.
TDD це цикл з трьох кроків: написати маленький тест, що падає з очікуваної причини (red), найпростішим кодом зробити його зеленим (green), прибрати дублювання й покращити дизайн під захистом тестів (refactor). Головна користь у фідбеку на дизайн, покриття тут лише побічний ефект: код, який важко протестувати першим, зазвичай має забагато залежностей. Найкраще працює для доменної логіки з чіткими правилами, гірше для дослідницького коду й UI.
Pest це надбудова над PHPUnit: з кожного файлу з `it()`/`test()` він генерує клас, що успадковує `TestCase`, і запускає його раннером PHPUnit з тим самим `phpunit.xml`. Тому класові тести працюють поруч без змін, асерти PHPUnit доступні, а Pest додає `expect()`-ланцюжки, datasets, хуки, `arch()`, мутаційне тестування й профілювання.
Хороший unit-тест перевіряє одну поведінку через публічний інтерфейс, складається з трьох частин (arrange, act, assert), має назву-речення про правило, не залежить від часу, бази й інших тестів і падає лише тоді, коли поведінка справді зламалась.
Покриття рахує, які рядки виконались під час тестів, але не чи помітив би тест їхню зміну: тест без перевірок дає 100%. Мутаційне тестування вносить у код дрібні зміни (мутанти: `>=` на `>`, `+` на `-`, `true` на `false`) і проганяє тести; мутант, на якому все зелене, вижив і показує неперевірене місце. У PHP це Infection і вбудований у Pest `--mutate`; частку вбитих мутантів показує MSI.
Архітектурний тест перевіряє будову коду, а не поведінку: хто кого імпортує, чи класи final, чи є strict_types, чи не лишився `dd()`. У Pest це `arch()->expect('App\Domain')->not->toUse('Illuminate')`: файли читаються парсером, код не виконується. Ними охороняють межі шарів і модулів, які інакше тихо зникають, а наявні порушення вносять у `ignoring()` явним списком.
Кожен тест має сам створювати рівно ті дані, від яких залежить його перевірка, і показувати їх у тексті тесту. Для моделей це фабрики зі станами (`Vacancy::factory()->published()`), для доменних обʼєктів у unit-тестах builder або Object Mother з розумними значеннями за замовчуванням. Спільні фікстури на весь набір зручні лише для довідників, бо інакше тести неявно залежать від чужих даних і ламаються від кожної зміни.
Спершу зафіксувати те, що код робить зараз, а не те, що він мав би робити: characterization-тести (Майкл Фезерс) або golden master на рівні HTTP чи консольної команди. Далі знайти шви (seams), через які можна підставити залежності, мінімальними безпечними рефакторингами винести логіку в класи, що тестуються, і лише тоді міняти поведінку. Нову логіку пишуть поруч і з тестами (sprout method, wrap method), а не всередині старого моноліту.
Спершу виміряти (`pest --profile`, час по джобах CI), потім прибрати дешеві втрати: хешування паролів, міграції на кожен тест, справжні `sleep()` і мережу, зайві дані з фабрик. Далі паралелити (`--parallel`, шардування на кілька машин), тримаючи тести ізольованими: своя база на процес, без спільних файлів і статики. Надійність тримається на правилі «flaky-тест це баг з власником», а не на автоматичних повторах.
Про тести питають на всіх рівнях. Junior просять написати простий тест, Middle пояснити різницю між моком і фейком, Senior розповісти, як тримати набір тестів швидким і надійним на великому проєкті.