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

Middle: питання на співбесіді з теми «Тестування»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

3 питання

vi.fn() - функція-шпигун: запам'ятовує виклики й може повертати задане значення.

const onSave = vi.fn();
saveForm({ name: 'Олена' }, onSave);

expect(onSave).toHaveBeenCalledOnce();
expect(onSave).toHaveBeenCalledWith({ name: 'Олена' });

vi.spyOn() - стежити за методом існуючого об'єкта чи підмінити його:

const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
// ...
expect(spy).toHaveBeenCalled();

vi.mock() - замінити весь модуль:

import { getRates } from './api';
import { convert } from './converter';

vi.mock('./api', () => ({
  getRates: vi.fn().mockResolvedValue({ USD: 41.5 }),
}));

it('конвертує за курсом', async () => {
  expect(await convert(100, 'USD')).toBe(4150);
});

vi.mock піднімається на початок файлу - він виконується до імпортів, навіть якщо записаний нижче. Тому всередині фабрики не можна використовувати змінні з файлу: для цього є vi.hoisted().

fetch - підмінити глобальну функцію:

vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
  ok: true,
  json: async () => ({ id: 1 }),
}));

Для багатьох запитів зручніше MSW (Mock Service Worker): він перехоплює запити на мережевому рівні, і код працює зі справжнім fetch.

Фейкові таймери - не чекати по-справжньому:

it('зберігає чернетку через 2 секунди', () => {
  vi.useFakeTimers();
  const save = vi.fn();
  const autosave = debounce(save, 2000);

  autosave('текст');
  vi.advanceTimersByTime(1999);
  expect(save).not.toHaveBeenCalled();

  vi.advanceTimersByTime(1);
  expect(save).toHaveBeenCalledWith('текст');

  vi.useRealTimers();
});

vi.setSystemTime(new Date('2026-10-04')) фіксує «поточну» дату - для коду, що залежить від Date.now().

Прибирати за собою: моки, що «протікають» між тестами, - головна причина тестів, які падають лише в певному порядку:

afterEach(() => {
  vi.restoreAllMocks();
  vi.unstubAllGlobals();
});

Або в конфігурації: restoreMocks: true.

Міра: мокайте межі системи - мережу, час, сховище, сторонні SDK. Якщо в тесті замоковано все, він перевіряє лише те, що моки викликаються, а не те, що код працює.

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

Код, що працює з DOM (Alpine-компоненти, скрипти на сторінках Blade, веб-компоненти), потребує документа. У Node.js його немає - тому тест запускають у середовищі, що імітує браузер.

Середовища Vitest:

Середовище Що це Особливості
node без DOM (за замовчуванням) для чистої логіки
jsdom реалізація DOM на JavaScript найповніша сумісність, повільніший
happy-dom легша реалізація DOM швидший, але деякі API відсутні чи поводяться інакше
Browser Mode справжній браузер через Playwright реальний рендеринг, макет, події
// vitest.config.js
export default defineConfig({
  test: { environment: 'jsdom' },
});

Чи лише для одного файлу - коментарем на початку: // @vitest-environment happy-dom.

Що jsdom і happy-dom не вміють: макет і розміри (getBoundingClientRect повертає нулі), прокрутку, IntersectionObserver, CSS-анімації, справжню навігацію. Тести, що від цього залежать, - для браузера.

DOM Testing Library - пошук елементів так, як їх бачить користувач:

import { screen, within } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import { mountSubscribeForm } from './subscribe';

it('показує помилку для неправильної пошти', async () => {
  document.body.innerHTML = '<div id="app"></div>';
  mountSubscribeForm(document.getElementById('app'));

  const user = userEvent.setup();
  await user.type(screen.getByLabelText('Email'), 'not-an-email');
  await user.click(screen.getByRole('button', { name: 'Підписатися' }));

  expect(await screen.findByRole('alert')).toHaveTextContent('Неправильна адреса');
});

Пріоритет запитів:

  1. getByRole з name - як елемент бачать допоміжні технології;
  2. getByLabelText - поля форм;
  3. getByText - звичайний текст;
  4. getByTestId - останній варіант, коли нічого іншого немає.

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

getBy / queryBy / findBy: getBy кидає помилку, якщо елемента немає; queryBy повертає null (для перевірки відсутності); findBy - асинхронний, чекає появи елемента.

@testing-library/jest-dom додає зручні матчери: toBeVisible, toBeDisabled, toHaveValue, toHaveTextContent (працює і з Vitest).

Не тестуйте розмітку: перевірки на кшталт «третій div має клас error» ламаються від кожної зміни верстки, хоча поведінка не змінилася.

Докладніше в документації: DOM Testing Library

Playwright керує справжніми браузерами (Chromium, Firefox, WebKit) і перевіряє застосунок так, як ним користується людина.

import { test, expect } from '@playwright/test';

test('користувач оформлює замовлення', async ({ page }) => {
  await page.goto('/products/42');
  await page.getByRole('button', { name: 'Додати в кошик' }).click();
  await page.getByRole('link', { name: 'Кошик' }).click();

  await expect(page.getByRole('heading', { name: 'Кошик' })).toBeVisible();
  await expect(page.getByTestId('cart-total')).toHaveText('1 999,00 ₴');
});

Локатори описують, як знайти елемент, а не сам елемент: пошук повторюється щоразу, коли до локатора звертаються. Пріоритет той самий, що в Testing Library: getByRole, getByLabel, getByText, і лише потім getByTestId. CSS-селектори на кшталт .btn-primary > span ламаються від зміни верстки.

Автоочікування - головна причина стабільності. Перед click() Playwright сам чекає, доки елемент буде прикріплений до DOM, видимий, стабільний (без анімації), увімкнений і не перекритий іншим елементом. Перевірки expect(locator).toHaveText() повторюються, доки не пройдуть чи не мине тайм-аут.

Звідки беруться нестабільні (flaky) тести:

Причина Як виправити
page.waitForTimeout(2000) чекати на стан: await expect(locator).toBeVisible()
перевірка значення один раз: expect(await el.textContent()).toBe(...) перевірка, що повторюється: await expect(el).toHaveText(...)
тести залежать один від одного чи від спільних даних кожен тест створює свої дані, ізольований стан входу
реальні сторонні сервіси (платежі, карти) підміна через page.route()
анімації й час вимкнути анімації, фіксувати час через page.clock

Повтор входу в кожному тесті повільний - стан автентифікації зберігають один раз (storageState) і перевикористовують.

Діагностика: trace: 'on-first-retry' у конфігурації записує трасу (знімки DOM, мережу, консоль для кожного кроку) при повторі. npx playwright show-trace показує, що саме сталося в CI.

retries у CI допомагає пережити рідкісні збої, але тест, що проходить лише з другої спроби, - сигнал, а не норма: такі тести варто відстежувати й виправляти.

Скільки E2E-тестів писати: небагато - для критичних сценаріїв. Вони найповільніші й найдорожчі в підтримці; решту поведінки дешевше перевіряти на нижчих рівнях.

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