Skip to main content

Типи фронтенд-тестування

Фронтенд-тестування перевіряє компоненти, логіку і взаємодію з користувачем на кількох рівнях, щоб знаходити баги до того, як вони потрапляють у продакшн.

Теорія

TL;DR

  • Аналогія: юніт-тести перевіряють двигун окремо, інтеграційні перевіряють чи двигун і трансмісія працюють разом, E2E тести їдуть на машині по реальній дорозі
  • Юніт-тести мокають залежності і виконуються менш ніж за 1ms; інтеграційні з'єднують реальні модулі; E2E запускають справжній браузер і займають секунди
  • Стартовий розподіл: 70% юніт, 20% інтеграційні, 10% E2E
  • Кожен тип ловить різне: юніт - помилки в логіці, інтеграційні - неправильну взаємодію між модулями, E2E - зламані сценарії користувача

Швидкий приклад

Юніт-тест з Jest і React Testing Library:

tsx
// Button.tsx const Button = ({ onClick, label }: { onClick: () => void; label: string }) => ( <button onClick={onClick}>{label}</button> ); // Button.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import Button from './Button'; test('викликає onClick при кліку', () => { const handleClick = jest.fn(); render(<Button onClick={handleClick} label="Натисни мене" />); fireEvent.click(screen.getByRole('button')); expect(handleClick).toHaveBeenCalledTimes(1); // проходить ✅ });

Цей тест рендерить компонент, симулює клік і перевіряє що колбек спрацював один раз. Браузер не потрібен, мережевих викликів немає.

Ключова різниця

Юніт-тести ізолюють одну одиницю коду (функцію, хук, компонент), мокаючи все навколо. Інтеграційні тести з'єднують реальні модулі і дозволяють їм взаємодіяти між собою. E2E тести відкривають справжній браузер і симулюють поведінку користувача від логіну до оформлення замовлення. Тут є компроміс між швидкістю і реалізмом: юніт-тести завершуються менш ніж за 1ms, інтеграційні займають 10-100ms, E2E можуть виконуватись кілька секунд за сценарій.

Коли використовувати

  • Чиста функція або утиліта: юніт-тест (найшвидший фідбек, найпростіше дебажити)
  • Компонент що використовує дочірні компоненти або хук: інтеграційний тест (реальний потік даних, без моків для внутрішніх частин)
  • Сценарій користувача - логін, оформлення замовлення, відправка форми: E2E тест (ловить layout shift, проблеми з редиректами, специфічні баги браузера)
  • Стабільність UI між деплоями: snapshot тест (ловить випадкові зміни класів або розмітки)
  • Точна піксельна відповідність: visual regression тест з Percy або Chromatic

Таблиця порівняння

ТипОбластьШвидкістьНестабільністьІнструментиЩо ловить
UnitФункція/хук/компонент<1msНизькаJest, Vitest, RTLЛогічні помилки
Integration2+ компоненти або сервіси10-100msСередняJest + MSW, RTLФорма не проходить API валідацію
E2EПовний сценарій у браузері1-10sВисокаPlaywright, CypressКнопка логіну зламана в mobile Safari
SnapshotСеріалізований рендер1-5msНизькаJest snapshotsCSS клас перейменовано, стилі зламались
VisualПіксельні скріншоти100ms-1sСередняPercy, ChromaticЗміна товщини шрифту зсуває верстку

Правило вибору: юніт для логіки, інтеграційні для взаємодії між частинами, E2E для критичних сценаріїв, snapshot/visual для стабільності UI.

Як тести працюють під капотом

Jest виконується в Node.js і використовує jsdom для симуляції DOM без реального браузера. Саме тому юніт і інтеграційні тести швидкі. Playwright і Cypress запускають справжній Chromium, WebKit або Firefox через браузерні API і вбудовують скрипти для контролю вкладок і перехоплення мережевих подій. Моки використовують JavaScript Proxy для перехоплення викликів до того, як вони досягають реального HTTP.

Типові помилки

Тестування деталей реалізації замість поведінки. Найпоширеніша помилка в юніт-тестах - перевіряти внутрішній стан або мок-функцію на кшталт setState напряму.

tsx
// Погано: зламається якщо поміняти useState на useReducer test('оновлює стан', () => { const setState = jest.fn(); expect(setState).toHaveBeenCalled(); // ❌ тестує як, а не що }); // Добре: тестує що користувач бачить насправді test('показує оновлений текст', () => { fireEvent.click(button); expect(screen.getByText('Оновлено')).toBeVisible(); // ✅ стабільно при рефакторингу });

Реальні API виклики в юніт-тестах. Без моків тест робить справжні HTTP запити. Це додає 500ms+ на тест, ламається при відсутності мережі і може попасти в продакшн базу даних. Використовуй MSW або jest.mock для перехоплення запитів.

Надмірне використання E2E. Деякі команди пишуть E2E на все, бо так здається реалістичніше. Але E2E тести в 10 разів повільніші і падають через race condition і проблеми з таймінгом, не пов'язані з реальними багами. Тримай E2E для 5-10 критичних сценаріїв.

Snapshot на все дерево компонентів. Snapshot повної сторінки змінюється щоразу, коли змінюється будь-який дочірній компонент. Розробники оновлюють snapshot файли, не читаючи що саме змінилось. Через деякий час ніхто не довіряє цим тестам і вони перетворюються на шум. Краще робити snapshot серіалізованих даних або невеликих стабільних частин UI.

Де зустрічається у реальних проектах

  • React застосунки: RTL для поведінки компонентів, MSW для API моків (Netflix, Shopify)
  • Next.js: Playwright для E2E сторінок (документація самого Vercel)
  • Vue/Nuxt: Vitest для юніт тестів, Cypress для E2E (підхід GitLab)
  • SvelteKit: Web Test Runner для юніт, Playwright для E2E (офіційні стартери)

Питання на співбесіді

Q: Що таке піраміда тестування (testing pyramid) і чому рекомендується розподіл 70/20/10?
A: Піраміда (Mike Cohn) розміщує найбільше тестів внизу, де вони найдешевші і найшвидші. Юніт-тести дешево писати і вони виконуються за мілісекунди. E2E дорогі в підтримці і повільні. Розподіл відображає ROI - витрати проти покриття, а не догму.

Q: Як зменшити нестабільність E2E тестів?
A: Не використовуй фіксовані setTimeout. Використовуй waitFor або вбудований auto-waiting Playwright. Додай retry логіку в CI. Використовуй --trace в Playwright щоб записати що сталось при падінні - без цього дебажити E2E наосліп.

Q: Коли MSW, а коли jest.mock для мокування API?
A: MSW перехоплює на рівні мережі і працює в браузері і Node одночасно, тому підходить для інтеграційних тестів що використовують fetch або axios. jest.mock працює на рівні модулів і простіший для юніт-тестів що викликають сервісну функцію напряму.

Q: Як тестувати кастомний React хук?
A: Використовуй renderHook з React Testing Library: const { result } = renderHook(useCounter); act(() => result.current.increment()); expect(result.current.count).toBe(1);

Q (senior): Спроектуй стратегію тестування для e-commerce чекауту. Яким буде розподіл і чому?
A: 70% юніт для математики кошика, логіки знижок і розрахунків ціни. 20% інтеграційні для форми і моків платіжного шлюзу. 10% E2E для сценарію від додавання в кошик до оплати, бо тут живе дохід. Джуніорська відповідь - рівне покриття всіх типів. Сеніорська відповідь оптимізує витрати: юніти дешеві, E2E ловить те що юніти не можуть.

Приклади

Базовий: юніт-тест для утилітарної функції

ts
// priceUtils.ts export function applyDiscount(price: number, discount: number): number { if (discount < 0 || discount > 100) throw new Error('Невірна знижка'); return price * (1 - discount / 100); } // priceUtils.test.ts import { applyDiscount } from './priceUtils'; test('застосовує знижку 20% правильно', () => { expect(applyDiscount(100, 20)).toBe(80); // ✅ }); test('кидає помилку при невірній знижці', () => { expect(() => applyDiscount(100, 150)).toThrow('Невірна знижка'); // ✅ });

Чиста функція без компонентів, без DOM, без моків. Виконується менш ніж за 1ms і дає миттєвий фідбек по логіці.

Середній: інтеграційний тест для форми з валідацією

tsx
// UserForm.tsx - форма з React Hook Form import { useForm } from 'react-hook-form'; type FormData = { email: string; age: number }; const UserForm = () => { const { register, handleSubmit, formState: { errors } } = useForm<FormData>(); const onSubmit = (data: FormData) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> <label htmlFor="email">Email</label> <input id="email" {...register('email', { required: true })} /> <label htmlFor="age">Вік</label> <input id="age" type="number" {...register('age', { min: 18 })} /> {errors.age && <span>Мінімальний вік - 18</span>} <button type="submit">Відправити</button> </form> ); }; // UserForm.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import UserForm from './UserForm'; test('показує помилку валідації якщо вік менше 18', async () => { render(<UserForm />); fireEvent.change(screen.getByLabelText(/вік/i), { target: { value: '16' } }); fireEvent.click(screen.getByRole('button', { name: /відправити/i })); expect(await screen.findByText(/мінімальний вік/i)).toBeInTheDocument(); // ❌ блокує відправку });

Це інтеграційний тест: перевіряє компонент форми разом з логікою валідації React Hook Form. Бібліотека не мокається, реальна валідація виконується. В цьому і є відмінність від юніт-тесту.

Просунутий: E2E тест Playwright для сценарію логіну

ts
// login.spec.ts - E2E тест Playwright import { test, expect } from '@playwright/test'; test('користувач може залогінитись і потрапити на дашборд', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Пароль').fill('password123'); await page.getByRole('button', { name: /увійти/i }).click(); // auto-waiting без фіксованих таймаутів await expect(page).toHaveURL('/dashboard'); await expect(page.getByText('Ласкаво просимо')).toBeVisible(); // ✅ });

Playwright відкриває справжній Chromium, заповнює поля як реальний користувач і перевіряє результуючий URL і вміст сторінки. Це ловить те, що юніт-тести не можуть: логіку редиректів, обробку сесії на сервері і layout shift на різних розмірах екрану.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?