Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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: рев'ю помічає порушення вибірково й залежить від уважності, а тест перевіряє кожен коміт. Архітектурні рішення, записані тестом, переживають зміну команди - їх не треба переказувати новачкам.

Обережно: правило без причини дратує й обходиться. Кожне має відповідати реальній домовленості, а не смаку.

Докладніше в документації: Тестування: вступ