Патерни тестів і стилістика
Структура кейсу
Section titled “Структура кейсу”AAA (Arrange–Act–Assert)
Section titled “AAA (Arrange–Act–Assert)”// Arrangerender(<LoginForm />);// Actawait userEvent.type(screen.getByLabelText(/email/i), 'a@b.com');await userEvent.click(screen.getByRole('button', { name: /sign in/i }));// Assertexpect(await screen.findByText(/welcome/i)).toBeInTheDocument();GWT (Given–When–Then)
Section titled “GWT (Given–When–Then)”// Givenconst cart = new Cart();cart.add(item);// Whencart.checkout();// Thenexpect(cart.status).toBe('paid');Іменування
Section titled “Іменування”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 }) => { /* ... */ })const cases = [ { role: 'admin', canSee: true }, { role: 'viewer', canSee: false },];
describe('User role permissions', () => { cases.forEach(({ role, canSee }) => { it(`renders correct visibility for role "${role}"`, () => {...}); });});Селектори та a11y-first
Section titled “Селектори та a11y-first”- Перевага:
getByRole,getByLabelText,getByText→ відображають реальний UX. getByTestId- тільки коли інакше неможливо (іменуйте осмислено).- Перевіряємо публічні стани:
disabled,aria-busy,aria-expanded,aria-invalid.
Взаємодії користувача
Section titled “Взаємодії користувача”- Використовуємо
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 глибоко --- краще тестувати через поведінку.
Матчери та перевірки
Section titled “Матчери та перевірки”- Підключені jest-dom матчери:
toBeInTheDocument,toBeDisabled,toHaveTextContent,toHaveAttribute,toHaveAccessibleName. - Перевірка викликів:
expect(api.create).toHaveBeenCalledWith({ id: 1 });Снепшоти — лише точкові
Section titled “Снепшоти — лише точкові”- Тільки малі та стабільні структури (об’єкти, короткі DOM-фрагменти).
- Не знімати великі дерева DOM/стилі --- ламкі та малокорисні.
Організація в файлі
Section titled “Організація в файлі”- Порядок: імпорти → моки → хелпери →
describeблоки. - Уникай дублювань: спільний
renderWithProviders/builders уtests/setup/. beforeEach/afterEach- тільки коли справді спільний сетап.
Детермінізм
Section titled “Детермінізм”- Фіксуй дату/час, якщо залежиш від них:
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 “Приклади мікропатернів”Варіативність пропів
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();Debounce
Section titled “Debounce”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();A11y атрибути
Section titled “A11y атрибути”render(<Accordion />);const btn = screen.getByRole('button', { name: /faq/i });await userEvent.click(btn);expect(btn).toHaveAttribute('aria-expanded', 'true');Короткий DO/DON’T
Section titled “Короткий DO/DON’T”DO
- Пиши тести, які читаються як специфікація.
- Використовуй a11y-селектори та
userEvent. - Контролюй час/рандом/мережу для детермінізму.
- Роби табличні тести замість копіпейсту.
DON’T
- Не тестуй внутрішній state/імплементацію.
- Не роби великі снепшоти.
- Не використовуй довільні таймаути.
- Не залишай
console.logу тестах.