Архітектурні принципи
Принцип ізоляції (Single Responsibility & Determinism)
Section titled “Принцип ізоляції (Single Responsibility & Determinism)”- Кожен модуль має виконувати одну логічну задачу — тоді його можна перевірити ізольовано.
- Функції не повинні залежати від глобального стану (
window,document,localStorage,Date.now()). - Для повторюваних або зовнішніх ресурсів — ін’єкція залежностей (dependency injection).
export const getUserAge = () => new Date().getFullYear() - user.birthYear;Для перевірки необхідний мок Date.
export const getUserAge = (birthYear: number, currentYear: number = new Date().getFullYear()) => currentYear - birthYear;Тепер функцію можна протестувати без моків Date.
Розділення шарів (Separation of Concerns)
Section titled “Розділення шарів (Separation of Concerns)”Виносимо логіку з React-компонентів у хуки, утиліти або сервіси:
| Рівень | Вміст | Тестуємо |
|---|---|---|
| UI (React) | Візуалізація, події, виклики хуків | поведінку (рендер, клік, стан) |
| Hook | локальна логіка (стан, ефекти, таймери, фетчі) | зміну станів і сайд-ефекти |
| Service / SDK | бізнес-логіка, API-виклики, кеш | чисту логіку, правильні запити |
| Utility | чисті функції, парсери, обчислення | вхід → вихід |
Інверсія залежностей
Section titled “Інверсія залежностей”Компоненти й модулі не повинні напряму викликати конкретні API або контексти — замість цього отримують абстракцію.
await fetch('/api/users');import { apiClient } from '@/sdk/apiClient';await apiClient.get('/users');У тестах можна легко змінити реалізацію:
vi.mock('@/sdk/apiClient', () => ({ apiClient: { get: vi.fn().mockResolvedValue(mockUsers) },}));Контрольованість побічних ефектів
Section titled “Контрольованість побічних ефектів”Усе, що виходить за межі компонента/функції, вважається “побічним ефектом”:
HTTP-запит- робота з
LocalStorage - зміна маршруту
- таймер (
setTimeout,setInterval) - генерація випадкових значень
Pure functions first
Section titled “Pure functions first”Перед тим як писати компонент, подумай:
“Чи можу я винести логіку в чисту функцію і протестувати її без DOM?”
Якщо так — це найкраще рішення. Чисті функції — фундамент для unit-тестів:
- не змінюють вхідні параметри;
- не залежать від зовнішнього стану;
- завжди повертають однаковий результат при однакових даних;
Передбачуваний стан
Section titled “Передбачуваний стан”- Не допускаємо “магічних” змін стану.
useStateіuseReducerвикористовуємо тільки в хуках або компонентах.- Будь-який стан має бути детермінований зовнішнім входом (props, event, API).
Мінімізація глобальних синглтонів
Section titled “Мінімізація глобальних синглтонів”- Не використовувати глобальні об’єкти напряму (
window,document,navigator). - Для глобальних поведінок — створювати адаптери:
export const storage = { get: (key: string) => localStorage.getItem(key), set: (key: string, value: string) => localStorage.setItem(key, value),};В тестах: vi.spyOn(storage, 'get').mockReturnValue('mock');
Small and Composable
Section titled “Small and Composable”- Кожен модуль має бути малим — легше покрити тестом і перевикористати.
- Якщо функція робить кілька речей — розділи.
- Краще 5 простих тестів на 5 функцій, ніж 1 тест на гігантський сценарій.
Мінімізація моків
Section titled “Мінімізація моків”Моки — зло, коли їх багато.
Вони приховують реальну поведінку й ускладнюють рефакторинг.
Використовуємо моки тільки коли:
- є побічний ефект (API, localStorage, таймер, контекст),
- тест має бути детермінованим.
Все інше — краще реально виконувати.
Layered test strategy
Section titled “Layered test strategy”- Unit — ізольовані функції/хуки (Vitest, msw/mock).
- Integration — взаємодія кількох компонентів (Testing Library + msw).
- E2E — повна користувацька історія (Playwright).
Наші unit-тести покривають 70–80% функціональної логіки, решта — на інтеграційних.
Антипатерни, яких уникати
Section titled “Антипатерни, яких уникати”| Антипатерн | Чому шкідливо |
|---|---|
| “God component” — логіка + UI + API разом | важко тестувати, переписувати, ізолювати |
Внутрішні селектори у тестах (getByTestId) | крихкі, прив’язані до реалізації |
| Моки глибоких бібліотек (React, router, form) | ризик хибних позитивів |
Нескінченні waitFor | флейки та затримки |
| Snapshot-тести на великі DOM | ламаються при будь-якому рефакторингу |
Стандартна структура проєкту для тестованості
Section titled “Стандартна структура проєкту для тестованості”src/ components/ Button.tsx __tests__/Button.test.tsx hooks/ useToggle.ts __tests__/useToggle.test.ts utils/ formatDate.ts __tests__/formatDate.test.ts sdk/ apiClient.ts __tests__/apiClient.node.test.ts- UI → jsdom
- SDK / utils → node
__tests__поруч або в окремій папці — вирішує команда, але схема однакова
Коротке резюме принципів
Section titled “Коротке резюме принципів”- Маленькі незалежні модулі.
- Логіка поза компонентом.
- Ін’єкція залежностей замість прямого доступу.
- Мінімум побічних ефектів.
- Орієнтація на поведінку, а не реалізацію.
- Детермінізм і стабільність.