<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
ТЕСТУВАННЯ · ОСНОВИ

Види тестівTest types

Unit, integration, feature і e2e відрізняються тим, скільки справжнього коду бере участь у перевірці. Від цього залежать швидкість тесту, те, що він здатен зловити, і те, від чого він ламається.

8 хв читання Junior → Middle
01 · ЩО ЦЕ

Скільки справжнього коду в тесті

Вид тесту визначає межа: що тест запускає по-справжньому, а що лишає за дужками. бере один клас або невелику групу класів без бази, мережі й фреймворку. додає одну справжню залежність: базу, кеш, файлову систему чи HTTP-клієнт. у Laravel надсилає запит крізь увесь застосунок, від маршруту до відповіді. [[E2E-тест|e2e]] відкриває справжній браузер і клікає по сторінці.

СЛОВНИКUnit-тест. Тест одного класу або невеликої групи класів у памʼяті, без бази, мережі, файлів і фреймворку. Залежності, що виходять назовні, підміняють дублерами.
СЛОВНИКIntegration-тест. Тест, який перевіряє ваш код разом з однією справжньою зовнішньою залежністю: базою, кешем, чергою, файлами чи чужим API.
СЛОВНИКFeature-тест. У Laravel: тест, що надсилає HTTP-запит або запускає команду крізь увесь застосунок, з маршрутизацією, middleware, валідацією й базою. Деінде його звуть функціональним або end-to-end на рівні HTTP.

Чим більше справжнього коду, тим більше тест ловить і тим дорожче коштує. Unit-тест іде мілісекунди й падає лише від зміни логіки в своєму класі, зате не помітить зламаної міграції. Feature-тест побачить і міграцію, і валідацію, і права доступу, але йде в десятки разів довше, а коли падає, причину доводиться шукати по всьому стеку. E2E ловить ще й зламаний JavaScript, проте найповільніший і найчастіше стає .

СЛОВНИКFlaky-тест. Тест, який то проходить, то падає на тому самому коді. Причини: час, випадкові дані, порядок тестів, мережа, гонки в браузері.

У Laravel тести лежать у двох папках, і в проєкті з Pest різницю між ними задає tests/Pest.php. Його типовий вміст розширює класом Tests\TestCase лише tests/Feature: там є контейнер, фасади, конфіг, база й методи на кшталт $this->get(). Тести з tests/Unit цього класу не отримують і працюють як чистий PHPUnit, тож виклик фасаду там упаде з «A facade root has not been set». Integration-тести в Laravel зазвичай живуть у tests/Feature, бо їм потрібні контейнер і база.

Скільки тестів якого виду писати, сперечаються давно. радить багато unit-тестів і мало повільних, ставить у центр інтеграційні, бо вони ближчі до того, що бачить користувач. Обидві форми є на хабі розділу. На практиці відповідь залежить від того, де живе логіка: багатий домен добре покривається unit-тестами, а CRUD-контролер чесніше перевірити одним feature-тестом.

СЛОВНИКПіраміда тестів. Модель Майка Кона (Mike Cohn): широка основа з unit-тестів, менше інтеграційних, зовсім мало e2e.
СЛОВНИКТрофей тестів. Модель Кента Доддса (Kent C. Dodds): статичний аналіз в основі, найбільше інтеграційних тестів, менше unit і e2e.
02 · ВАРІАНТИ

Одне правило на трьох рівнях

Правило: вакансія зникає з каталогу через 60 днів після публікації. Його можна перевірити в доменному обʼєкті, у запиті до бази й на сторінці /jobs. Перемикайте вкладки й дивіться, скільки коду бере участь у кожному тесті і що саме може зламатися.

php JobExpiryTest.php Unit
1// tests/Unit/JobExpiryTest.php: без Laravel і без бази
2it('expires a job sixty days after publication', function () {
3 $job = new Job('Senior Laravel Engineer', new DateTimeImmutable('2026-09-01'));
4
5 expect($job->isExpiredAt(new DateTimeImmutable('2026-10-30')))->toBeFalse()
6 ->and($job->isExpiredAt(new DateTimeImmutable('2026-10-31')))->toBeTrue();
7});
ЩО ПЕРЕВІРЯЄ
Саме правило, і точно на межі: 59 днів ще можна, 60 уже ні. Дата приходить параметром, тож тест не залежить від сьогоднішнього дня й іде за мілісекунди.
КОЛИ БРАТИ
Для бізнес-правил: тарифи, статуси, межі дат, розрахунки. Тут зручно перебрати десяток крайніх випадків, бо кожен коштує майже нічого.
php JobExpiryTest.php Integration
1// tests/Feature/JobRepositoryTest.php: справжня база, без HTTP
2it('leaves expired jobs out of the visible list', function () {
3 $jobs = app(JobRepository::class);
4 $jobs->save(new Job('Senior Laravel Engineer', new DateTimeImmutable('2026-08-01')));
5 $jobs->save(new Job('Middle Symfony Developer', new DateTimeImmutable('2026-09-20')));
6
7 $visible = $jobs->visibleAt(new DateTimeImmutable('2026-10-05'));
8
9 expect(array_map(fn (Job $job) => $job->title, $visible))
10 ->toBe(['Middle Symfony Developer']);
11});
ЩО ПЕРЕВІРЯЄ
Що те саме правило правильно записане мовою бази: умова WHERE, формат дат, назви колонок. Unit-тест на Job цього не бачить, бо запит він не виконує.
КОЛИ БРАТИ
Для репозиторіїв, складних запитів, кешу, роботи з файлами: усюди, де помилка ховається в розмові з зовнішньою системою.
php JobExpiryTest.php Feature
1// tests/Feature/JobsPageTest.php: HTTP-запит крізь увесь Laravel
2it('shows only fresh jobs on the catalog page', function () {
3 $jobs = app(JobRepository::class);
4 $jobs->save(new Job('Senior Laravel Engineer', new DateTimeImmutable('-61 days')));
5 $jobs->save(new Job('Middle Symfony Developer', new DateTimeImmutable('-3 days')));
6
7 $this->get('/jobs')
8 ->assertOk()
9 ->assertSee('Middle Symfony Developer')
10 ->assertDontSee('Senior Laravel Engineer');
11});
ЩО ПЕРЕВІРЯЄ
Що користувач справді не побачить застарілу вакансію: маршрут, контролер, репозиторій і шаблон разом. Перевірки дивляться на HTML-відповідь, а не на внутрішні класи.
КОЛИ БРАТИ
Для того, що обіцяєте назовні: сторінки, API, форми, права доступу. Один такий тест на сценарій, а варіації правила лишайте unit-тестам.
03 · ЯК У PHP UKRAINE

Feature-тест сторінки вакансії

Вилку зарплати в PHP Ukraine перевіряють на двох рівнях. Unit-тест tests/Unit/Hiring/SalaryTest.php дивиться на сам value object: Salary::range(7000, 5000) міняє межі місцями й рахує середину. Feature-тест нижче проходить увесь шлях: вакансія приходить через справжній сценарій синхронізації в базу, сторінка віддається по HTTP, а вилка мусить опинитися в розмітці JobPosting для Google. Базу для кожного такого тесту готує tests/Pest.php: він підключає TestCase і RefreshDatabase лише для tests/Feature.

php JobsPagesTest.php фрагмент
1it('shows a job page with JobPosting structured data and redirects stale slugs', function () {
2 SeedsJobs::seed([← дані через справжній сценарій
3 ['title' => 'Senior Laravel Engineer', 'company' => 'Genesis', 'id' => 'j1', 'salary' => [5000, 7000], 'remote' => true, 'cities' => []],
4 ]);
5
6 $list = $this->get('/jobs');← HTTP крізь увесь застосунок
7 preg_match('~/jobs/(\d+)-([a-z0-9-]+)~', (string) $list->getContent(), $m);
8
9 $this->get("/jobs/{$m[1]}-{$m[2]}")
10 ->assertOk()
11 ->assertSee('Senior Laravel Engineer')
12 ->assertSee('Genesis')
13 ->assertSee('"@type":"JobPosting"', false)
14 ->assertSee('"jobLocationType":"TELECOMMUTE"', false)
15 ->assertSee('"minValue":5000', false)← вилка дійшла до JSON-LD
16 ->assertSee('Джерело')
17 ->assertSee('https://dou.test/j1');
18
19 $this->get("/jobs/{$m[1]}-wrong-slug")->assertRedirect("/jobs/{$m[1]}-{$m[2]}");← стара адреса веде на нову
20 $this->get('/jobs/999999-nothing')->assertNotFound();
21});
04 · ТИПОВІ ПОМИЛКИ

Де ламається баланс

✕ 1

Усе через HTTP

Кожне правило перевіряють feature-тестом: двадцять запитів до /checkout з різними сумами, щоб покрити тарифи доставки. Набір іде хвилинами, а коли тест падає, незрозуміло, зламався тариф, валідація чи шаблон.

Натомість: варіації правила перевіряйте unit-тестами на доменному класі, а на рівні HTTP лишіть один-два сценарії, які доводять, що все зʼєднано правильно.

✕ 2

Unit-тести, що нічого не ловлять

Контролер, який лише передає запит у сервіс, обкладений моками з усіх боків. Тест повторює реалізацію, проходить завжди й падає лише від рефакторингу. Зламаний маршрут чи міграцію він не помітить.

Натомість: тонкий код, що склеює фреймворк з доменом, перевіряйте feature-тестом. Unit-тести пишіть там, де є логіка.

✕ 3

Unit-тест, що потай ходить у базу

Тест лежить у tests/Unit, але через модель Eloquent чи хелпер now() тягне за собою Laravel і базу. Він повільний, залежить від порядку запуску й ламається від чужих змін.

Натомість: у tests/Unit тримайте лише код без фреймворку. Щойно тесту потрібні контейнер чи база, переносьте його в tests/Feature, а залежність від часу передавайте параметром або через Clock.

✕ 4

E2E на кожну кнопку

Браузерні тести перевіряють валідацію кожного поля форми. Вони йдуть хвилинами, падають від повільної анімації, і команда звикає перезапускати червоний CI.

Натомість: у браузері перевіряйте кілька критичних шляхів, як реєстрація чи оплата, і те, що без JavaScript не перевірити. Валідацію лишіть feature-тестам.

05 · ДЕРЕВО РІШЕНЬ

Який тест писати

1
Правило можна перевірити без бази, мережі й фреймворку?
Розрахунок, статус, межа дат, перетворення даних. Час і випадковість передаються параметром.
НІ →Integration або feature. Якщо правило живе в SQL чи в розмові з API, перевірте його на справжній залежності.
2
Цей код сам ухвалює рішення?
Контролер, що передає запит у сервіс, і Livewire-компонент без логіки нічого не вирішують.
НІ →Feature-тест. Склеювання найкраще видно, коли запит проходить увесь шлях.
3
Варіантів поведінки багато, а кожен feature-тест коштує сотні мілісекунд?
Двадцять тарифів, десять статусів, межі дат. Кожна варіація через HTTP додає час до набору.
НІ →Одного feature-тесту вистачить. Окремий unit-тест на одну умову нічого не додасть.
ТАК НА ВСІ →Unit-тести на кожну варіацію правила й один feature-тест, який доводить, що правило підключене до сторінки.
06 · СЛОВНИК
Unit-тест
Тест одного класу або невеликої групи класів у памʼяті, без бази, мережі, файлів і фреймворку. Залежності, що виходять назовні, підміняють дублерами.
Integration-тест
Тест, який перевіряє ваш код разом з однією справжньою зовнішньою залежністю: базою, кешем, чергою, файлами чи чужим API.
Feature-тест
У Laravel: тест, що надсилає HTTP-запит або запускає команду крізь увесь застосунок, з маршрутизацією, middleware, валідацією й базою. Деінде його звуть функціональним або end-to-end на рівні HTTP.
E2E-тест
End-to-end: справжній браузер відкриває сторінку, клікає й заповнює форми, як користувач. У PHP це Laravel Dusk, браузерні тести Pest або Playwright окремо від PHP.
Flaky-тест
Тест, який то проходить, то падає на тому самому коді. Причини: час, випадкові дані, порядок тестів, мережа, гонки в браузері.
Піраміда тестів
Модель Майка Кона (Mike Cohn): широка основа з unit-тестів, менше інтеграційних, зовсім мало e2e.
Трофей тестів
Модель Кента Доддса (Kent C. Dodds): статичний аналіз в основі, найбільше інтеграційних тестів, менше unit і e2e.
07 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з тестування →

Unit-тест перевіряє клас у памʼяті без фреймворку й бази та йде мілісекунди. Feature-тест розширює Tests\TestCase, піднімає застосунок і надсилає запит крізь маршрути, middleware, валідацію й базу. Перший точно показує, яке правило зламалось, другий доводить, що все зʼєднано правильно.

Бо tests/Pest.php розширює класом Tests\TestCase лише tests/Feature. Без нього ніхто не створює застосунок і не реєструє його в фасадах, тож Cache::get() падає з «A facade root has not been set». Тест, якому потрібен Laravel, належить до tests/Feature.

Integration-тест перевіряє ваш код разом з однією справжньою залежністю, наприклад репозиторій на справжній базі. Unit-тест з фейком не побачить помилки в SQL, а feature-тест побачить, але через десяток шарів, і ви довго шукатимете причину. Integration-тест ловить її точно й швидше.

Залежить від того, де живе ризик. Якщо в домені багато правил, основу дають unit-тести, бо кожна варіація коштує мілісекунди. Якщо застосунок переважно CRUD і логіка в запитах і валідації, вигідніші feature-тести, як радить трофей. В обох випадках e2e лишають для кількох критичних шляхів, а статичний аналіз ловить цілий клас помилок без жодного тесту.