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

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 чи справжній браузер).

Докладніше в документації: Vitest: початок роботи

Головна небезпека в асинхронних тестах - тест завершується раніше, ніж перевірка. Тест, у якому жодна перевірка не виконалася, вважається пройденим.

Неправильно:

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(...)) - повторює перевірку, доки вона не пройде чи не мине час, коли результат з'являється не одразу.

Порада: напишіть тест, переконайтеся, що він падає, якщо зламати код. Тест, який ніколи не падав, міг нічого не перевіряти.

Докладніше в документації: Vitest: API 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): інструмент навмисно змінює код (> на >=, + на -) і перевіряє, чи впаде хоч один тест. Якщо мутація «вижила» - тести цей код насправді не перевіряють.

Докладніше в документації: Vitest: покриття