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

Які бувають тести і в якій пропорції їх писати: піраміда чи трофей?

Вид тесту визначає, скільки справжнього коду бере участь у перевірці: unit бере клас у памʼяті, integration додає одну справжню залежність (базу, кеш, файли), feature проганяє HTTP-запит крізь увесь застосунок, e2e клікає в браузері. Піраміда радить багато unit-тестів і мало повільних, трофей ставить у центр інтеграційні; пропорцію обирають за тим, де в проєкті живуть баги.

Чим unit-тест відрізняється від інтеграційного й від e2e?
У вас тисяча unit-тестів, усі зелені, а на проді впала сторінка оформлення замовлення. Як таке могло статися?
Набір тестів іде пʼятнадцять хвилин, і майже всі тести ходять через HTTP. Що б ви змінили?
Ви чули про піраміду тестів? Чи досі вона актуальна?
unit integration feature e2e піраміда тестів трофей тестів

Вид тесту визначає не розмір обʼєкта перевірки, а те, скільки справжнього коду бере в ній участь. Unit-тест створює клас через new і працює в памʼяті, без бази, мережі й фреймворку. Integration-тест додає одну справжню залежність: репозиторій пише в тестову базу, сервіс читає файл. Feature-тест у Laravel надсилає HTTP-запит крізь маршрутизацію, middleware, валідацію, контролер і базу. E2E відкриває сторінку в браузері й клікає, як користувач. Що більше справжнього коду, то більше тест ловить і то дорожче коштує: unit іде мілісекунди, feature десятки чи сотні мілісекунд, браузерний тест секунди.

Від цього залежить і те, що кожен рівень здатен зловити. Unit-тест точно показує, яке правило зламалось, але не помітить міграції, що перейменувала колонку, чи маршруту без auth. Feature-тест побачить усе разом, проте коли він падає, ще треба знайти, в якому з десяти шарів помилка. У Laravel цей поділ ще й технічний: згенерований tests/Pest.php містить pest()->extend(Tests\TestCase::class)->in('Feature'), тож у tests/Unit застосунок не піднято, і фасад там падає з «A facade root has not been set».

Пропорцію описують двома моделями. Піраміда Майка Кона (Mike Cohn) радить широку основу з unit-тестів, менше інтеграційних і зовсім мало e2e. Трофей Кента Доддса (Kent C. Dodds) кладе в основу статичний аналіз, а найбільше місця віддає інтеграційним тестам, бо вони ближчі до того, що бачить користувач, і ловлять помилки на стиках. Обидві моделі правильні для свого коду. Якщо в домені тарифи, статуси й розрахунки, вигідно перебирати варіації unit-тестами, де кожна коштує майже нічого. Якщо застосунок переважно CRUD, баги живуть у валідації, політиках і запитах, і їх ловлять feature-тести.

Робочий підхід поєднує обидва: варіації правила перевіряти на найнижчому рівні, де воно живе, а на рівні HTTP лишати один-два сценарії, які доводять, що правило підключене до сторінки. Тонкий код, що лише склеює фреймворк з доменом, краще покрити feature-тестом, ніж обкласти моками. Браузерні тести лишають для кількох критичних шляхів і того, що залежить від JavaScript.

Перевірити, чи пропорція в проєкті здорова, можна за симптомами. Тести ламаються від рефакторингу без зміни поведінки: забагато моків на внутрішні класи. Набір іде хвилинами: варіації правил женуть через HTTP або браузер. Продакшн падає при зеленому CI на маршрутах і правах доступу: бракує feature-тестів саме там.

// Одне правило: вакансія видима 60 днів після публікації.

// tests/Unit/JobExpiryTest.php: без Laravel і бази, дата приходить параметром.
it('expires a job on the sixtieth day', function (string $today, bool $expired) {
    $job = new Job('Senior Laravel Engineer', new DateTimeImmutable('2026-09-01'));

    expect($job->isExpiredAt(new DateTimeImmutable($today)))->toBe($expired);
})->with([
    'день 59' => ['2026-10-30', false],
    'день 60' => ['2026-10-31', true],   // межу перевіряємо з обох боків
]);

// tests/Feature/JobsPageTest.php: той самий сценарій крізь маршрут,
// контролер і шаблон. Один тест на зʼєднання, без перебору варіацій.
it('hides expired jobs from the catalog', function () {
    $this->travelTo('2026-10-31 12:00');
    $jobs = app(JobRepository::class);
    $jobs->save(new Job('Senior Laravel Engineer', new DateTimeImmutable('2026-09-01')));
    $jobs->save(new Job('Middle Symfony Developer', new DateTimeImmutable('2026-10-20')));

    $this->get('/jobs')
        ->assertOk()
        ->assertSee('Middle Symfony Developer')
        ->assertDontSee('Senior Laravel Engineer');
});
Що межа між видами проходить по тому, що тест запускає по-справжньому, а не по розміру класу: unit без бази, мережі й фреймворку, integration з однією справжньою залежністю, feature через HTTP-вхід застосунку, e2e у справжньому браузері.
Що кожен рівень має ціну й силу: unit іде мілісекунди й точно показує зламане правило, але не бачить міграції чи маршруту; feature бачить усе разом, але повільніший і гірше локалізує помилку.
Що піраміда (Майк Кон) і трофей (Кент Доддс) відповідають на одне питання по-різному, і кандидат пояснює, від чого залежить вибір: багатий домен тягне до піраміди, CRUD з логікою в запитах і валідації тягне до трофея.
Що в Laravel поділ технічний: `tests/Pest.php` розширює `Tests\TestCase` лише для `tests/Feature`, тому в `tests/Unit` немає контейнера, фасадів і бази.
Що статичний аналіз (PHPStan) теж рівень перевірки: він ловить виклики на null і невідомі методи без жодного запуску.
Називати unit-тестом будь-який тест одного класу, навіть якщо він через модель Eloquent пише в базу й залежить від міграцій.
Казати «правильна пропорція 70/20/10» як закон, не пояснивши, звідки цифри і чому в CRUD-застосунку вони можуть бути шкідливими.
Перевіряти кожну варіацію правила feature-тестом: двадцять POST-запитів на двадцять тарифів доставки замість двадцяти рядків датасету в unit-тесті й одного feature-тесту.
Обкласти моками тонкий контролер і вважати його покритим: такий тест повторює реалізацію і не помічає зламаного маршруту чи middleware.
Ставити e2e-тести на валідацію кожного поля форми: вони повільні, падають від анімацій, і команда звикає перезапускати червоний CI.
ПОРАДА

Відповідайте через одне правило на трьох рівнях: «вакансія зникає з каталогу через 60 днів». Межу 59 і 60 днів перевіряю unit-тестом на доменному обʼєкті, запит до бази integration-тестом репозиторію, а що користувач не бачить застарілу вакансію, одним feature-тестом сторінки. Так видно, що ви обираєте рівень під ризик, а не під моду.

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

Unit, integration, feature й e2e відрізняються тим, що запускається по-справжньому: клас у памʼяті, одна справжня залежність, увесь застосунок через HTTP, браузер. Цифри піраміди це орієнтир для коду з багатою доменною логікою, а не норма; у CRUD-застосунку вигідніший трофей з feature-тестами в центрі. E2E найповільніші й найкрихкіші, тому їх тримають для кількох критичних шляхів.