Вид тесту визначає не розмір обʼєкта перевірки, а те, скільки справжнього коду бере в ній участь. 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');
});
Відповідайте через одне правило на трьох рівнях: «вакансія зникає з каталогу через 60 днів». Межу 59 і 60 днів перевіряю unit-тестом на доменному обʼєкті, запит до бази integration-тестом репозиторію, а що користувач не бачить застарілу вакансію, одним feature-тестом сторінки. Так видно, що ви обираєте рівень під ризик, а не під моду.