Конфігурація тестів
Обов’язкові залежності
Section titled “Обов’язкові залежності”pnpm add -D vitest@^4 vite@^5 @types/node typescriptpnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom whatwg-fetchpnpm add -D mswpnpm add -D vite-tsconfig-pathsСтруктурні файли
Section titled “Структурні файли”vitest.config.ts (Vitest v4 з projects)
/// <reference types="vitest" />import { defineConfig } from "vitest/config";import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({plugins: [tsconfigPaths()],// загальні дефолти (успадковуються проектами, але їх можна перевизначати)test: { globals: true, include: ["src/**/*.{test,spec}.{ts,tsx}"], setupFiles: ["./setupTests.ts"], reporters: ["default", ["junit", { outputFile: "reports/junit-unit.xml" }]], coverage: { provider: "v8", reportsDirectory: "reports/coverage", reporter: ["text", "html", "lcov", "json-summary", "cobertura"], lines: 80, functions: 80, branches: 75, statements: 80, exclude: [ "**/e2e/**", "**/stories/**", "**/*.d.ts", "**/msw/**", "**/fixtures/**", "**/factories/**", "**/.next/**", "**/dist/**", "**/build/**", ], }, testTimeout: 10_000, hookTimeout: 10_000, css: false, isolate: true,},
// окремі середовища projects: [ { test: { name: "jsdom", environment: "jsdom", include: ["src/**/*.{test,spec}.{ts,tsx}"], exclude: ["src/**/*.node.{test,spec}.{ts,tsx}"], environmentOptions: { jsdom: { pretendToBeVisual: true, url: "http://localhost/" }, }, }, }, { test: { name: "node", environment: "node", include: ["src/**/*.node.{test,spec}.{ts,tsx}"], }, }, ],});import "@testing-library/jest-dom/vitest";import "whatwg-fetch";import { afterEach, beforeAll, afterAll, vi } from "vitest";import { cleanup } from "@testing-library/react";
// Моки відсутніх у jsdom APIObject.defineProperty(window, "matchMedia", {writable: true,value: (query: string) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), // deprecated addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(),}),});class MockResizeObserver {observe = vi.fn();unobserve = vi.fn();disconnect = vi.fn();}class MockIntersectionObserver {observe = vi.fn();unobserve = vi.fn();disconnect = vi.fn();takeRecords = vi.fn(() => []);}(globalThis as any).ResizeObserver = MockResizeObserver;(globalThis as any).IntersectionObserver = MockIntersectionObserver;
// Чистимо DOM після кожного тестуafterEach(() => cleanup());
// Фейл на неочікувані console.error / warn (гейт якості)const origErr = console.error, origWarn = console.warn;beforeAll(() => { console.error = (...args: any[]) => { origErr(...args); throw new Error("console.error during test:" + args.join("")); }; console.warn = (...args: any[]) => { origWarn(...args); if (process.env.CI === "true") throw new Error("console.warn in CI:" + args.join("")); };});afterAll(() => { console.error = origErr; console.warn = origWarn;});
// Опційний автосетап MSW, якщо існують хендлери(async () => { try { const [{ setupServer }, { handlers }] = await Promise.all([ import("msw/node"), import("./tests/msw/handlers"), ]); const server = setupServer(...handlers); beforeAll(() => server.listen({ onUnhandledRequest: "error" })); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); } catch { /* MSW не налаштований — ок */ }})();import { http, HttpResponse } from 'msw';
export const handlers = [http.get('/api/health', () => HttpResponse.json({ ok: true })),// інші REST/GraphQL хендлери];TZ=UTCNODE_ENV=test# будь-які тестові змінні. Не секрети продакшну!API_BASE_URL=http://localhostВикористовуйте
.env.testчерез власний конфіг-завантажувач або бібліотеку, якщо додаток читає ENV.
Сценарії запуску
Section titled “Сценарії запуску”{ "scripts": { "test": "vitest", "test:run": "vitest run", "test:ui": "vitest --ui", "test:jsdom": "vitest run --project jsdom", "test:node": "vitest run --project node", "test:cov": "vitest run --coverage", "test:watch": "vitest --watch" }}Політика моків і таймерів
Section titled “Політика моків і таймерів”- Мережа: за замовчуванням MSW. Для простих unit -
vi.mock('module', ...). - Таймери: у тестах, де є debounce/throttle -
vi.useFakeTimers()→vi.advanceTimersByTime(ms)→vi.useRealTimers(). - Час/дата: ін’єнктуй або фіксуй через
vi.setSystemTime(new Date('2020-01-01T00:00:00Z')).
A11y та селектори
Section titled “A11y та селектори”- Використовуємо byRole / byLabelText / byText;
getByTestId- тільки якщо інакше неможливо. - Додаємо
aria-*атрибути в компоненті, якщо це покращує керованість і тести.
У разі використання сторонніх бібліотек для a11y, наприклад @radix-ui,
перетестовувати елементи керованості, focus та інші атрибути для a11y нема потреби.
Перевіряємо тільки поведінку - toBeInTheDocument, наявність класу чи атрибуту при зміні стану.
Покриття (coverage)
Section titled “Покриття (coverage)”- Пороги: утиліти/бізнес-логіка ≥ 90%, хуки ≥ 85%, компоненти 70—80%, загалом репо 80%+.
- Виключення вказані у
coverage.exclude. Для файлів-бочок (ре-експортів) можна використовувати/* istanbul ignore file */.
Продуктивність і стабільність
Section titled “Продуктивність і стабільність”isolate: true--- ізоляція кожного тест-файлу.- Мінімізуємо глобальні стейти у тестах; очищаємо моки
vi.clearAllMocks()уbeforeEach, якщо потрібно. - Уникаємо довільних
await new Promise(r => setTimeout(r, X)); замість цього -findBy*/waitForабо фейкові таймери.
Рекомендації для CI/Docker
Section titled “Рекомендації для CI/Docker”- Запуск у контейнері з Node LTS,
TZ=UTC,LANG=en_US.UTF-8. - Виводити артефакти у
reports/coverageтаreports/junit-*.xml. - Для Vitest v4 - можна запускати паралельно
--project jsdomі--project node.
Хелпер для провайдерів
Section titled “Хелпер для провайдерів”Мінімальний хелпер для провайдерів (опціонально)
import { render } from "@testing-library/react";import type { ReactNode } from "react";
export function renderWithProviders(ui: ReactNode) {// тут можна обгорнути у ThemeProvider/Router/QueryClientProvider тощоreturn render(<>{ui}</>);}Використовуй у тестах замість render там, де потрібні провайдери.