Senior: питання на співбесіді з теми «Тестування»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
6 питань
BDD розширює TDD, зміщуючи фокус на поведінку системи з погляду бізнесу/користувача, а не на технічні деталі. Сценарії описують зрозумілою мовою (Gherkin: Given-When-Then).
Scenario: Успішний вхід
Given користувач зареєстрований
When він вводить правильні дані
Then він потрапляє на дашборд
У PHP-екосистемі - Behat. Pest також заохочує «describe behavior» стиль:
it('redirects to dashboard after login', function () {
// ...
});
Цінність BDD - спільна мова між розробниками, QA та бізнесом; тести стають живою документацією очікуваної поведінки.
Для Livewire - хелпер livewire() (із pest-plugin-livewire):
livewire(SearchPosts::class)
->set('query', 'laravel')
->assertSee('Laravel Queues')
->call('clear')
->assertSet('query', '')
->assertDispatched('posts-updated');
Для Filament - спершу автентифікуйте користувача, потім тестуйте сторінки ресурсів:
livewire(CreatePost::class)
->fillForm(['title' => 'Hello'])
->call('create')
->assertHasNoFormErrors();
livewire(ListPosts::class)
->callAction(TestAction::make('publish')->table($post))
->assertNotified();
Ключові асерти: assertSet, assertSee, assertDispatched, assertHasFormErrors, assertCanSeeTableRecords.
Усі три замінюють справжню залежність у тесті, але перевіряють різне.
- Fake - робоча спрощена реалізація.
Mail::fake()справді приймає листи, а тест перевіряє результат: що пішло, кому, з чим. - Mock - об'єкт з очікуваннями, заданими заздалегідь: «метод
chargeбуде викликано раз з сумою 100». Не збулося - тест падає. - Spy - записує всі виклики, а перевірки пишуть після дії. Як mock, але читається в порядку «дія → перевірка».
$this->mock(PaymentGateway::class, function (MockInterface $mock) {
$mock->expects('charge')->with(10000)->andReturn(new Charge('ch_1'));
});
Чому надмір моків шкодить: тест, що мокає п'ять залежностей і перевіряє порядок їхніх викликів, фіксує реалізацію, а не поведінку. Будь-який рефакторинг ламає тест, хоча для користувача нічого не змінилось, - і команда звикає «лагодити тести», а не довіряти їм.
Практичні правила:
- Мокати межі системи - зовнішні API, платежі, пошту, час - а не власні класи.
- Віддавати перевагу фейкам і перевірці стану: що записано в базу, що повернуто.
- Не мокати те, чим не володієте, напряму: краще обгорнути чужий SDK своїм інтерфейсом і підміняти його.
- Хоча б кілька тестів мають проганяти справжній ланцюжок без підмін - інакше зелений набір нічого не каже про продакшен.
Спершу виміряти, потім лагодити найповільніше.
1. Знайти повільні тести:
php artisan test --profile
Зазвичай кілька тестів забирають більшу частину часу - справжні HTTP-запити, sleep(), сідер з тисячами рядків.
2. Паралельний запуск:
php artisan test --parallel
Laravel створює окрему тестову базу на кожен процес. Тести мають бути незалежними: спільні файли, кеш чи зовнішні ресурси під паралеллю дадуть «моргаючі» падіння.
3. Дешевші налаштування:
WithCachedConfig(Laravel 13) - конфігурація збирається один раз, а не на кожен тест;LazilyRefreshDatabase- тести без бази не мігрують її;- драйвери
arrayдля кешу й сесії,syncдля черги, низькийBCRYPT_ROUNDSдля хешування.
4. Менше роботи в самих тестах:
- створювати рівно ті дані, які перевіряються, а не викликати важкий сідер;
make()замістьcreate(), де база не потрібна;- підміняти мережу (
Http::fake()) і час (travel()), а не чекати.
5. Ширше: на CI розбивати набір між машинами (--shard у Pest), а локально запускати лише тести, яких торкнулися зміни (--tia у Pest 5).
Швидкий набір - не розкіш: тести, які йдуть 25 хвилин, розробники перестають запускати локально.
Зробити так, щоб N+1 ламав тест, а не тихо пригальмовував сторінку в проді.
1. Заборонити ліниве завантаження поза продакшеном:
// AppServiceProvider::boot()
Model::preventLazyLoading(! $this->app->isProduction());
Тепер звернення до незавантаженого зв'язку в циклі кидає LazyLoadingViolationException - і будь-який тест сторінки, що це робить, впаде. Часто вмикають разом Model::shouldBeStrict(), який ще й ловить звернення до відсутніх атрибутів.
2. Обмежити кількість запитів для критичної сторінки (Laravel 13):
it('loads the orders page in a fixed number of queries', function () {
Order::factory()->count(20)->hasItems(3)->create();
$this->expectsDatabaseQueryCount(4);
$this->actingAs(User::factory()->admin()->create())
->get('/admin/orders')
->assertOk();
});
Важливо: тестові дані мають бути «множинними». N+1 не видно на одному записі - один пост з одним коментарем дасть два запити і з with(), і без. Тест має створювати кілька записів, щоб різниця між «2 запити» і «21 запит» проявилася.
Альтернатива в Laravel 12+ - Model::automaticallyEagerLoadRelationships(), що сам довантажує зв'язки для всієї колекції. Але тест з лічильником запитів корисний і тоді: він фіксує, що сторінка не деградує з часом.
Це тести не поведінки, а структури коду: які класи де лежать, від чого залежать, що їм заборонено. У Pest вони пишуться функцією arch().
arch('models extend Eloquent')
->expect('App\Models')
->toExtend(Illuminate\Database\Eloquent\Model::class);
arch('no debugging leftovers')
->expect(['dd', 'dump', 'ray', 'var_dump'])
->not->toBeUsed();
arch('controllers stay thin')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Support\Facades\DB');
arch()->preset()->laravel();
arch()->preset()->security();
Що ними зручно фіксувати:
- забуті
dd()іdump(), які інакше потрапляють у продакшен; - межі шарів: домен не залежить від HTTP, контролери не пишуть сирі запити;
- угоди: усі завдання реалізують
ShouldQueue, усі enum - уApp\Enums, класиfinalчиreadonly, де так домовились; - небезпечні функції:
eval,exec,md5для паролів (пресетsecurity).
Навіщо, якщо є code review: рев'ю помічає порушення вибірково й залежить від уважності, а тест перевіряє кожен коміт. Архітектурні рішення, записані тестом, переживають зміну команди - їх не треба переказувати новачкам.
Обережно: правило без причини дратує й обходиться. Кожне має відповідати реальній домовленості, а не смаку.