Skip to content

Архітектурні принципи

Принцип ізоляції (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чисті функції, парсери, обчисленнявхід → вихід

Компоненти й модулі не повинні напряму викликати конкретні API або контексти — замість цього отримують абстракцію.

Погано
await fetch('/api/users');
Добре
import { apiClient } from '@/sdk/apiClient';
await apiClient.get('/users');

У тестах можна легко змінити реалізацію:

setupTests.ts
vi.mock('@/sdk/apiClient', () => ({
apiClient: { get: vi.fn().mockResolvedValue(mockUsers) },
}));

Контрольованість побічних ефектів

Section titled “Контрольованість побічних ефектів”

Усе, що виходить за межі компонента/функції, вважається “побічним ефектом”:

  • HTTP-запит
  • робота з LocalStorage
  • зміна маршруту
  • таймер (setTimeout, setInterval)
  • генерація випадкових значень

Перед тим як писати компонент, подумай:

“Чи можу я винести логіку в чисту функцію і протестувати її без DOM?”

Якщо так — це найкраще рішення. Чисті функції — фундамент для unit-тестів:

  • не змінюють вхідні параметри;
  • не залежать від зовнішнього стану;
  • завжди повертають однаковий результат при однакових даних;
  • Не допускаємо “магічних” змін стану.
  • useState і useReducer використовуємо тільки в хуках або компонентах.
  • Будь-який стан має бути детермінований зовнішнім входом (props, event, API).

Мінімізація глобальних синглтонів

Section titled “Мінімізація глобальних синглтонів”
  • Не використовувати глобальні об’єкти напряму (window, document, navigator).
  • Для глобальних поведінок — створювати адаптери:
storage.ts
export const storage = {
get: (key: string) => localStorage.getItem(key),
set: (key: string, value: string) => localStorage.setItem(key, value),
};

В тестах: vi.spyOn(storage, 'get').mockReturnValue('mock');

  • Кожен модуль має бути малим — легше покрити тестом і перевикористати.
  • Якщо функція робить кілька речей — розділи.
  • Краще 5 простих тестів на 5 функцій, ніж 1 тест на гігантський сценарій.

Моки — зло, коли їх багато.
Вони приховують реальну поведінку й ускладнюють рефакторинг.

Використовуємо моки тільки коли:

  • є побічний ефект (API, localStorage, таймер, контекст),
  • тест має бути детермінованим.

Все інше — краще реально виконувати.

  1. Unit — ізольовані функції/хуки (Vitest, msw/mock).
  2. Integration — взаємодія кількох компонентів (Testing Library + msw).
  3. 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 “Коротке резюме принципів”
  1. Маленькі незалежні модулі.
  2. Логіка поза компонентом.
  3. Ін’єкція залежностей замість прямого доступу.
  4. Мінімум побічних ефектів.
  5. Орієнтація на поведінку, а не реалізацію.
  6. Детермінізм і стабільність.