Junior: питання на співбесіді з теми «Тестування»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Фронтенд-код ламається так само, як серверний: форма перестає відправлятися, фільтр показує не ті дані, кнопка «Оплатити» неактивна. Без тестів про це дізнаються користувачі, а кожен рефакторинг стає ризиком.
Рівні тестів:
| Рівень | Що перевіряє | Інструменти | Швидкість |
|---|---|---|---|
| unit | одна функція чи модуль окремо: форматування ціни, валідація, розрахунок кошика | Vitest, Jest | мілісекунди |
| компонентні / інтеграційні | кілька частин разом у DOM: компонент з формою, відправка, повідомлення про помилку | Vitest + Testing Library, jsdom чи справжній браузер | десятки мілісекунд |
| end-to-end (E2E) | увесь застосунок у справжньому браузері з бекендом: вхід, оформлення замовлення | Playwright, Cypress, Pest browser tests | секунди |
Піраміда радить багато дешевих unit-тестів, менше інтеграційних і кілька E2E: що вище рівень, то тест повільніший, крихкіший і дорожчий у підтримці.
«Тестовий трофей» (Kent C. Dodds) - альтернативний погляд для фронтенду: найбільше користі дають інтеграційні тести, бо unit-тести окремих функцій інтерфейсу часто перевіряють деталі реалізації, а не те, що бачить користувач. Основа трофея - статичний аналіз: TypeScript і ESLint ловлять цілий клас помилок без жодного тесту.
Що тестувати насамперед:
- чисту логіку без DOM: розрахунки, перетворення даних, парсинг - найдешевші й найстабільніші тести;
- критичні сценарії користувача (вхід, оплата, відправка форми) - кількома E2E-тестами;
- місця, де вже були баги: тест на баг гарантує, що він не повернеться.
Що не тестувати:
- бібліотеки й фреймворк - вони протестовані авторами;
- точну розмітку й стилі - такі тести падають від кожної зміни дизайну;
- приватні деталі реалізації, які користувач не бачить.
Для Laravel-проєкту: бекенд покривають Pest-тестами, а для фронтенду часто достатньо unit-тестів складної логіки в Vitest і браузерних тестів основних сторінок - вони перевіряють і JavaScript, і серверну частину одночасно.
Докладніше в документації: Martin Fowler: The Practical Test Pyramid
Vitest - тестовий фреймворк, побудований на Vite. Він використовує ту саму конфігурацію, плагіни й псевдоніми шляхів, що й збирання застосунку, тож тести бачать код так само, як браузер.
npm install -D vitest
// resources/js/utils/price.js
export function formatPrice(cents) {
return `${(cents / 100).toFixed(2)} грн`;
}
// resources/js/utils/price.test.js
import { describe, it, expect } from 'vitest';
import { formatPrice } from './price';
describe('formatPrice', () => {
it('перетворює копійки на гривні', () => {
expect(formatPrice(12345)).toBe('123.45 грн');
});
it('показує нуль', () => {
expect(formatPrice(0)).toBe('0.00 грн');
});
});
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
Запуск:
npx vitest- режим спостереження: при зміні файлу перезапускаються лише пов'язані з ним тести;npx vitest run- один прогін, для CI;npx vitest price- лише файли, у назві яких єprice.
Основні перевірки:
| Матчер | Для чого |
|---|---|
toBe |
примітиви, порівняння за === |
toEqual |
об'єкти й масиви за вмістом |
toStrictEqual |
те саме, але враховує undefined-поля і класи |
toContain, toHaveLength |
масиви й рядки |
toThrow |
функція кидає помилку |
toMatchObject |
об'єкт містить указані поля |
Типова помилка - expect({ a: 1 }).toBe({ a: 1 }): це різні об'єкти, тест падає. Для об'єктів - toEqual.
Чому Vitest, а не Jest, у проєкті на Vite:
- одна конфігурація:
vite.config.jsз псевдонімом@/уже працює в тестах, не треба дублювати її для Jest; - ES-модулі й TypeScript без Babel і додаткових трансформерів;
- швидкий режим спостереження завдяки графу модулів Vite;
- API сумісний з Jest (
describe,it,expect, моки) - перехід зазвичай механічний.
Середовище: за замовчуванням тести виконуються в Node.js. Для коду, що працює з DOM, потрібне браузерне середовище (jsdom, happy-dom чи справжній браузер).
Головна небезпека в асинхронних тестах - тест завершується раніше, ніж перевірка. Тест, у якому жодна перевірка не виконалася, вважається пройденим.
Неправильно:
it('завантажує користувача', () => {
fetchUser(1).then((user) => {
expect(user.name).toBe('Олена'); // виконається після завершення тесту
});
});
Тест «зелений» навіть тоді, коли fetchUser повертає зовсім іншого користувача.
Правильно - async/await:
it('завантажує користувача', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Олена');
});
Тестовий фреймворк чекає на Promise, який повертає функція тесту.
Перевірка відхиленого Promise:
it('кидає помилку для неіснуючого користувача', async () => {
await expect(fetchUser(999)).rejects.toThrow('Not found');
});
it('повертає дані', async () => {
await expect(fetchUser(1)).resolves.toMatchObject({ id: 1 });
});
await перед expect(...).rejects обов'язковий - без нього перевірка знову не встигне виконатися.
Варіант з try/catch потребує захисту:
it('кидає помилку', async () => {
expect.assertions(1); // тест упаде, якщо перевірок буде не рівно одна
try {
await fetchUser(999);
} catch (error) {
expect(error.message).toBe('Not found');
}
});
Без expect.assertions(1) тест пройде, якщо fetchUser помилково не кине винятку - блок catch просто не виконається.
Таймери й затримки не варто чекати по-справжньому (await new Promise(r => setTimeout(r, 3000))): тест стане повільним і нестабільним. Для цього є фейкові таймери.
Перевірка, що все дочекалося:
expect.hasAssertions()- хоча б одна перевірка виконалася;vi.waitFor(() => expect(...))- повторює перевірку, доки вона не пройде чи не мине час, коли результат з'являється не одразу.
Порада: напишіть тест, переконайтеся, що він падає, якщо зламати код. Тест, який ніколи не падав, міг нічого не перевіряти.
Покриття (coverage) - частка коду, яка виконалася під час тестів.
npm install -D @vitest/coverage-v8
npx vitest run --coverage
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
price.js | 100 | 75 | 100 | 100 | 12
cart.js | 62.50 | 50 | 66.66 | 62.50 | 18-24,31
Метрики:
| Метрика | Що рахує |
|---|---|
| Statements / Lines | виконані інструкції чи рядки |
| Functions | функції, які хоч раз викликали |
| Branches | гілки умов: обидві частини if/else, ? :, ??, && |
Branches - найкорисніша: 100% рядків може бути навіть тоді, коли гілка else жодного разу не перевірялася.
Провайдери у Vitest: v8 (за замовчуванням, використовує вбудоване покриття рушія, швидкий) і istanbul (інструментує код, працює в будь-якому середовищі).
Що покриття НЕ показує:
- чи є перевірки: тест, що викликає функцію без жодного
expect, дає 100% покриття і нічого не перевіряє; - чи перевірки правильні: код виконано, але очікуваний результат у тесті міг бути скопійований з помилкової реалізації;
- граничні випадки: рядок
price * qtyпокрито, але ніхто не перевірив від'ємну кількість, нуль, дробові значення; - поєднання умов: обидві гілки двох
ifпокрито окремо, але не всі чотири комбінації; - інтеграцію: кожен модуль покрито, а разом вони не працюють.
Як користуватися з розумом:
- дивитися на непокриті рядки, а не на відсоток: звіт показує, яку логіку забули перевірити;
- поріг у CI (
coverage.thresholds) - захист від падіння покриття, але не ціль. Вимога «90% будь-якою ціною» породжує тести без сенсу; - виключити згенерований код, конфігурації, типи (
coverage.exclude), щоб вони не спотворювали картину.
Що краще оцінює якість тестів - мутаційне тестування (Stryker для JavaScript, Pest Mutate для PHP): інструмент навмисно змінює код (> на >=, + на -) і перевіряє, чи впаде хоч один тест. Якщо мутація «вижила» - тести цей код насправді не перевіряють.