Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Типи фронтенд-тестування». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Фронтенд-тестування (frontend testing)** перевіряє компоненти, логіку і сценарії користувача на різних рівнях, щоб ловити баги до продакшну. ```ts // Юніт: ізольований, найшвидший test('застосовує знижку', () => { expect(applyDiscount(100, 20)).toBe(80); // <1ms ✅ }); ``` **Правило:** 70% юніт тести для логіки, 20% інтеграційні для взаємодії компонентів, 10% E2E для критичних сценаріїв.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Фронтенд-тестування** перевіряє компоненти, логіку і взаємодію з користувачем на кількох рівнях, щоб знаходити баги до того, як вони потрапляють у продакшн. ## Теорія ### 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 | Логічні помилки | | Integration | 2+ компоненти або сервіси | 10-100ms | Середня | Jest + MSW, RTL | Форма не проходить API валідацію | | E2E | Повний сценарій у браузері | 1-10s | Висока | Playwright, Cypress | Кнопка логіну зламана в mobile Safari | | Snapshot | Серіалізований рендер | 1-5ms | Низька | Jest snapshots | CSS клас перейменовано, стилі зламались | | 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 на різних розмірах екрану.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.