Skip to content

Патерни тестів і стилістика

Базовий формат
// Arrange
render(<LoginForm />);
// Act
await userEvent.type(screen.getByLabelText(/email/i), 'a@b.com');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
// Assert
expect(await screen.findByText(/welcome/i)).toBeInTheDocument();
доречний для бізнес-логіки
// Given
const cart = new Cart();
cart.add(item);
// When
cart.checkout();
// Then
expect(cart.status).toBe('paid');
  • describe - що тестуємо: <Component>, useHook, formatDate.
  • it/test - поведінка:
    it('disables submit while request is pending')
    it('sets isLoading to true')
  • Для варіантів - табличні тести:
it.each`
role | canSee
${'admin'} | ${true}
${'viewer'} | ${false}
`('role=$role → visible=$canSee', ({ role, canSee }) => { /* ... */ })
  • Перевага: getByRole, getByLabelText, getByText → відображають реальний UX.
  • getByTestId - тільки коли інакше неможливо (іменуйте осмислено).
  • Перевіряємо публічні стани: disabled, aria-busy, aria-expanded, aria-invalid.
  • Використовуємо userEvent, не fireEvent (окрім низькорівневих кейсів).
  • Клавіатура й фокус:
const user = userEvent.setup();
await user.tab();
expect(screen.getByRole('button', { name: /save/i })).toHaveFocus();

Асинхронність без флейків

Section titled “Асинхронність без флейків”
  • Для елементів, що з’являються — findBy* або waitFor:
await waitFor(() => expect(fetchSpy).toHaveBeenCalled());
  • Не використовувати «магічні» таймаути (setTimeout у тесті).
  • Для debounce/throttle - фейкові таймери:
vi.useFakeTimers();
await user.type(input, 'abc');
vi.advanceTimersByTime(300);
vi.useRealTimers();

Мінімум моків, максимум реальної поведінки

Section titled “Мінімум моків, максимум реальної поведінки”
  • Моки лише для побічних ефектів: мережа (MSW або vi.mock), час, сховище.
  • Не мокати фреймворки/React-router глибоко --- краще тестувати через поведінку.
  • Підключені jest-dom матчери: toBeInTheDocument, toBeDisabled, toHaveTextContent, toHaveAttribute, toHaveAccessibleName.
  • Перевірка викликів:
expect(api.create).toHaveBeenCalledWith({ id: 1 });

Снепшоти — лише точкові

Section titled “Снепшоти — лише точкові”
  • Тільки малі та стабільні структури (об’єкти, короткі DOM-фрагменти).
  • Не знімати великі дерева DOM/стилі --- ламкі та малокорисні.
  • Порядок: імпорти → моки → хелпери → describe блоки.
  • Уникай дублювань: спільний renderWithProviders/builders у tests/setup/.
  • beforeEach/afterEach - тільки коли справді спільний сетап.
  • Фіксуй дату/час, якщо залежиш від них:
vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
  • Випадковість - через ін’єкцію або seed.

Конкурентність та ретраї (обережно)

Section titled “Конкурентність та ретраї (обережно)”
  • Можна it.concurrent для незалежних швидких юнітів (без спільного стану).
  • Ретраї (test.retry) - тільки для тимчасової ізоляції flaky-кейсів, з окремою задачою на виправлення.

Перевірка помилок і логів

Section titled “Перевірка помилок і логів”
  • Будь-який неочікуваний console.error/console.warn у тесті - помилка (це вмикається у setupTests.ts).
  • Тести на помилки мають перевіряти поведінку UI (toast/alert), а не внутрішні винятки.

Приклади мікропатернів

Section titled “Приклади мікропатернів”
it.each([
{ mode: "compact", label: /more/i },
{ mode: "full", label: /show details/i },
])("shows correct label in $mode", ({ mode, label }) => {
render(<DetailsButton mode={mode as any} />);
expect(screen.getByRole("button", { name: label })).toBeInTheDocument();
});

Negative-first (перевірка заборон)

Section titled “Negative-first (перевірка заборон)”
render(<Submit disabled />);
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
vi.useFakeTimers();
const user = userEvent.setup({ delay: null });
render(<SearchBox />);
await user.type(screen.getByRole('searchbox'), 'abc');
vi.advanceTimersByTime(300);
expect(api.search).toHaveBeenCalledWith('abc');
vi.useRealTimers();

Асинхронний запит + стани

Section titled “Асинхронний запит + стани”
render(<UserList />);
expect(screen.getByRole('status', { name: /loading/i })).toBeInTheDocument();
expect(await screen.findByText(/alice/i)).toBeInTheDocument();
expect(screen.queryByRole('status', { name: /loading/i })).not.toBeInTheDocument();
render(<Accordion />);
const btn = screen.getByRole('button', { name: /faq/i });
await userEvent.click(btn);
expect(btn).toHaveAttribute('aria-expanded', 'true');

DO

  • Пиши тести, які читаються як специфікація.
  • Використовуй a11y-селектори та userEvent.
  • Контролюй час/рандом/мережу для детермінізму.
  • Роби табличні тести замість копіпейсту.

DON’T

  • Не тестуй внутрішній state/імплементацію.
  • Не роби великі снепшоти.
  • Не використовуй довільні таймаути.
  • Не залишай console.log у тестах.