Skip to content

Конфігурація тестів

Обов’язкові залежності

Section titled “Обов’язкові залежності”
Основні пакети
pnpm add -D vitest@^4 vite@^5 @types/node typescript
Бібліотеки
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom whatwg-fetch
Опційно
pnpm add -D msw
pnpm add -D vite-tsconfig-paths

vitest.config.ts (Vitest v4 з projects)

vitest.config.ts
/// <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}"],
},
},
],
});
setupTests.ts
import "@testing-library/jest-dom/vitest";
import "whatwg-fetch";
import { afterEach, beforeAll, afterAll, vi } from "vitest";
import { cleanup } from "@testing-library/react";
// Моки відсутніх у jsdom API
Object.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 не налаштований — ок */
}
})();
tests/msw/handlers.ts (опціонально)
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/health', () => HttpResponse.json({ ok: true })),
// інші REST/GraphQL хендлери
];
.env.test
TZ=UTC
NODE_ENV=test
# будь-які тестові змінні. Не секрети продакшну!
API_BASE_URL=http://localhost

Використовуйте .env.test через власний конфіг-завантажувач або бібліотеку, якщо додаток читає ENV.

package.json
{
"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')).
  • Використовуємо byRole / byLabelText / byText; getByTestId - тільки якщо інакше неможливо.
  • Додаємо aria-* атрибути в компоненті, якщо це покращує керованість і тести.

У разі використання сторонніх бібліотек для a11y, наприклад @radix-ui, перетестовувати елементи керованості, focus та інші атрибути для a11y нема потреби. Перевіряємо тільки поведінку - toBeInTheDocument, наявність класу чи атрибуту при зміні стану.

  • Пороги: утиліти/бізнес-логіка ≥ 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 або фейкові таймери.
  • Запуск у контейнері з Node LTS, TZ=UTC, LANG=en_US.UTF-8.
  • Виводити артефакти у reports/coverage та reports/junit-*.xml.
  • Для Vitest v4 - можна запускати паралельно --project jsdom і --project node.

Хелпер для провайдерів

Section titled “Хелпер для провайдерів”

Мінімальний хелпер для провайдерів (опціонально)

tests/setup/renderWithProviders.tsx
import { render } from "@testing-library/react";
import type { ReactNode } from "react";
export function renderWithProviders(ui: ReactNode) {
// тут можна обгорнути у ThemeProvider/Router/QueryClientProvider тощо
return render(<>{ui}</>);
}

Використовуй у тестах замість render там, де потрібні провайдери.