Skip to content

Тестування ​

У монорепозиторії кожен пакет тестується окремо: backend і frontend — через Vitest, shared і documentation — через вбудований node:test. Кореневий скрипт pnpm test запускає pnpm -r test для всіх пакетів (package.json). Нижче основна увага — backend.

Backend: конфігурація Vitest ​

Конфіг packages/backend/vitest.config.ts мінімальний:

  • environment: "node";
  • exclude: стандартні configDefaults.exclude плюс dist/**, **/*.redis.test.ts і **/*.pg.test.ts (їх запускають окремі test:redis і test:pg, див. розділ «Інтеграційні тести з Redis і PostgreSQL» нижче). Окремого include немає, тож Vitest підхоплює стандартні шаблони *.spec.ts / *.test.ts;
  • testTimeout: 15_000 — 15 с на тест замість стандартних 5 с: під паралельним навантаженням (typecheck + lint) 5 с не вистачало;
  • maxWorkers береться з VITEST_MAX_WORKERS, якщо змінну задано (у CI — 2); без неї діє значення Vitest за замовчуванням;
  • аліаси #app, #config, #vendor, #database, #drizzle вказують на відповідні каталоги src/, а #logger — на src/vendor/utils/logger.ts.

setupFiles і секції coverage у конфігу немає.

Скрипти пакета (packages/backend/package.json):

СкриптКоманда
testvitest run
test:watchvitest

Тести лежать поруч із кодом як *.spec.ts (наприклад, csrf-guard.ts і csrf-guard.spec.ts). Реальний прогін pnpm --filter backend test на гілці документації дав: 51 файл, 268 тестів, усі пройшли, ~2,2 с. Vitest запускає кожен файл в окремому воркері (ізоляція за замовчуванням).

Категорії тестів ​

КатегоріяПриклади
Старт сервера (vendor)server.spec.ts (handleError пише 422 до заголовків), router.spec.ts, static-server.spec.ts, http-validation.spec.ts
Маршрутизація / WSserialize-routes.spec.ts, ws-validation.spec.ts (422 і невиклик handler при невалідному payload), define-route.spec.ts
Сервісиauth-service.spec.ts, knowledge-base-service.spec.ts, push-subscription-service.spec.ts
Middlewarecsrf-guard.spec.ts (мокає #config/csrf.js і будує фейковий HttpContext), session-web.spec.ts
Валідація / адаптериarktype-adapter.spec.ts, ws-message-validator.spec.ts
Утилітиrate limit (rate-limit-store.spec.ts — обмежений стан, точний Retry-After, межа ключів; http-rate-limit.spec.ts, upgrade-rate-limit.spec.ts, server-rate-limit.spec.ts — охоплення L1; rate-limit.transport.spec.ts — справжній uWS: тіло POST не губиться, handshake 101/429), сесії/CSRF-токени, s3.spec.ts, logger.spec.ts

Патерни мокання ​

  • Barrel-модулі. Сервіси мокають #app/repositories/index.js і #app/transformers/index.js, а потім динамічно імпортують сервіс. Детально — у «Сервіси», «Репозиторії» і «Трансформери»; повний приклад — крок 9 у «Додавання фічі».
  • Окремі модулі. Деякі тести мокають конкретний файл, наприклад knowledge-base-service.spec.ts мокає #app/repositories/support/support-knowledge-repository.js, support-embedding-service.js і #logger.
  • Конфіг. vi.mock("#config/csrf.js", ...) у csrf-guard.spec.ts підставляє конфіг з enforce: true.
  • uWebSockets.js. server.spec.ts і http-validation.spec.ts мокають uWebSockets.js (App: () => ({})), маршрути, ws-handlers та HTTP-middleware, а HttpResponse підміняють об'єктом із vi.fn() для cork/writeStatus/writeHeader/end. Справжній сервер не стартує, портів ніхто не слухає (у spec-файлах немає викликів listen().
  • Часткові моки. vi.importActual зберігає решту модуля, підмінюючи одну функцію (getData у http-validation.spec.ts).

Жоден spec-файл не мокає #database/db.js і не згадує Redis, тож для pnpm --filter backend test не потрібні ні БД, ні Redis.

Інтеграційні тести з Redis і PostgreSQL ​

СуфіксКонфігСкриптЗміннаCI
*.redis.test.tsvitest.redis.config.tspnpm --filter backend test:redisREDIS_TEST_URLredis-tests.yml
*.pg.test.tsvitest.pg.config.tspnpm --filter backend test:pgPG_TEST_URLpg-tests.yml

Без змінної всі suites пропускаються (describe.skipIf), тож скрипти безпечно запускати будь-де. Файли виконуються послідовно (fileParallelism: false).

PostgreSQL. Перед тестами pg-global-setup.ts через PG_TEST_URL (адмінське з'єднання, зазвичай до бази postgres) створює тимчасову базу vibe_test_<random>, вмикає vector, застосовує міграції з src/drizzle/migrations у ту саму таблицю drizzle.__drizzle_migrations, що й db:migrate, і виставляє PGSQL_* на цю базу. Тому db з #database/db.js і всі репозиторії в тестах працюють зі справжньою схемою без моків. Після прогону база видаляється (DROP DATABASE … WITH (FORCE)). Потрібен образ з pgvector — той самий pgvector/pgvector:pg16, що в CI.

Локальний запуск:

bash
docker run -d --rm --name vibe-pg-test -e POSTGRES_PASSWORD=postgres \
  -p 127.0.0.1:55433:5432 pgvector/pgvector:pg16
PG_TEST_URL=postgres://postgres:postgres@127.0.0.1:55433/postgres pnpm --filter backend test:pg
docker stop vibe-pg-test

Новий pg-тест: файл <name>.pg.test.ts поруч із кодом, describe.skipIf(!hasPg) з pg-test-db.ts, #database/db.js імпортується динамічно всередині describe, а в afterAll — pool.end(). Тестові дані кожен файл створює й прибирає сам: база спільна для всього прогону. Зразок — harness.pg.test.ts.

Coverage ​

Backend і frontend мають скрипт test:coverage (vitest run --coverage з провайдером @vitest/coverage-v8, звіти text-summary у консоль і html у coverage/):

bash
pnpm --filter backend test:coverage
pnpm --filter frontend test:coverage

Порогу немає: CI лише публікує звіт. Coverage рахується по файлах, які завантажили тести, а не по всьому src/. Стартові значення (рядки, жовтень 2026): backend ~74 %, frontend ~51 %.

Інші пакети ​

  • shared — "test": "pnpm build && node --test test/*.test.js" (package.json): спочатку збірка, потім node:test над test/languages.test.js.
  • frontend — Vitest у jsdom поверх vite.config, з виключенням e2e/**, e2e-csp/** і e2e-security/** (vitest.config.ts); unit-тести в src/**/__tests__. Скрипти: test (vitest run), test:unit (vitest), test:e2e (playwright test). Playwright (playwright.config.ts) бере тести з e2e/, проєкти chromium, firefox, webkit і Mobile Safari, а webServer сам піднімає pnpm run dev на 5173 (на CI — pnpm run preview на 4173) або перевикористовує запущений. Backend Playwright не запускає. П'ятий проєкт mock (тести в e2e/mock/, лише chromium) перевіряє стани модалок входу, реєстрації та відновлення пароля на підроблених відповідях API: кожен запит до /api обслуговує сам тест через page.route (e2e/mock/support.ts), а запит без відповіді валить тест. Для нього конфіг збирає окрему копію SPA в dist-e2e-mock/ з VITE_AUTH_PHONE_ENABLED=true, VITE_BASE_URL на власний origin і вимкненим Sentry та віддає її через vite preview на порту 4174, тож результат не залежить від .env розробника. Тести фіксують поточну поведінку AuthModals.vue як є, зокрема й дивну (успішна відповідь без користувача показується як помилка; Enter у полі логіну нічого не робить); їхні очікування змінюються тільки свідомо, разом із рефакторингом цього файлу. Окремий конфіг playwright.live.config.ts з тестами в e2e-live/ (test:e2e:live) проганяє SPA через справжній бекенд із PostgreSQL (pgvector), Redis і поштовим приймачем: e2e-live/mail-sink.ts — мінімальний SMTP-сервер на 2526, у який бекенд шле листи (SMTP_IGNORE_TLS=true), тож тест читає посилання з листа так, як його натиснув би користувач. Скрипт збирає SPA без phone auth з origin http://localhost:3100, а webServer спершу створює базу vibe_e2e_live (db:create, db:migrate, db:seed --demo) і лише потім запускає бекенд з CSRF_ENFORCE=true; префікс ключів Redis новий на кожен запуск, тож лічильники throttle попереднього не заважають. Кожен браузер сценарію — окремий BrowserContext зі своєю адресою в X-Forwarded-For (TRUST_PROXY=true лише для localhost): інакше усі вони — 127.0.0.1, і ліміт 10 запитів на хвилину на групу auth зупиняє прогін. Сценарії: скидання пароля листом, зміна пароля на сторінці акаунта та «вийти всюди» закривають сесії інших браузерів (API відповідає 401, сторінка показує вхід; відкритий WebSocket відкликаної сесії сервер закриває лише на наступному запиті клієнта, див. WebSocket), старий пароль не діє, нова працює, посилання скидання одноразове; реєстрація → лист → підтвердження → вхід, де ранній вхід дає те саме 401, а повторний лист робить перше посилання недійсним; лічильник реєстрації на адресу справжній (четверта спроба за годину — 400). Потрібні PostgreSQL і Redis; у CI їх дає job e2e-live. CSP перевіряє окремий конфіг playwright.csp.config.ts з тестами в e2e-csp/: test:e2e:csp збирає production-статику (scripts/prod-local.sh --build-only) і двічі запускає бекенд з SERVE_STATIC=true — у режимі enforce і report-only. Перевіряються дозволені сценарії: реєстрація, WebSocket (до кінця handshake — очікується фрейм pong у відповідь на heartbeat ping), шрифти, service worker offline і зображення з CDN_URL (naturalWidth > 0; роль CDN грає заглушка e2e-csp/cdn-stub.mjs). Заборонені: inline-скрипт (effectiveDirective script-src-elem), fetch поза connect-src і зображення поза img-src на локальний probe-origin, з перевіркою disposition (enforce / report). Потрібні PostgreSQL і Redis. CSRF і service worker перевіряє playwright.security.config.ts з тестами в e2e-security/: test:e2e:security збирає production-статику і запускає бекенд з CSRF_ENFORCE=true, CSRF_STRICT_ORIGIN=true і CSRF_ALLOWED_ORIGINS; global-setup.ts створює два демо-акаунти через db:seed --demo. Сценарії: холодний вхід (init → 401 → login з токеном), logout → login, застарілий токен при живій сесії (рівно один повтор), втрачена сесія і зміна акаунта в іншій вкладці (без повтору, у другому випадку — перезавантаження під новим користувачем), відхилені мутації без побічного ефекту (чужий Origin, Sec-Fetch-Site: cross-site, без токена, без Origin), і відсутність API-відповідей у Cache Storage. «Інша вкладка» — page.request, що ділить cookie з контекстом браузера. Потрібні PostgreSQL і Redis.

Не покрито

Бекенд запускається з APP_ENV=local по http, тому production-гілка (upgrade-insecure-requests, HTTPS/WSS) і реальні S3/CDN (завантаження аватара, редирект /support/screenshot/:id) у браузерних тестах не перевіряються.

  • documentation — node --test "scripts/verify/*.test.mjs" (package.json) перевіряє хелпери скрипта verify-dist (lib.test.mjs).

CI і локальний запуск ​

У .github/workflows три workflow:

WorkflowКолиЩо робить
ci.ymlкожен PR і push у main, без фільтра pathsjob quality: pnpm install --frozen-lockfile → pnpm --filter shared build → format:check → typecheck → lint → test → test:coverage (artifact coverage); job e2e: shared build → pnpm --filter frontend build → playwright install --with-deps (усі браузери; кеш ~/.cache/ms-playwright за версією Playwright) → test:e2e на 5 проєктах (artifact playwright-report при падінні); job e2e-live (сервіси PostgreSQL з pgvector і Redis): pnpm --filter frontend test:e2e:live на chromium (artifact playwright-live-report при падінні)
pg-tests.ymlPR і push зі змінами backend/sharedpnpm --filter backend test:pg із сервісом pgvector/pgvector:pg16; єдине місце, де запускаються *.pg.test.ts
redis-tests.ymlPR і push зі змінами backend/sharedpnpm --filter backend test:redis із сервісом redis:7-alpine; єдине місце, де запускаються *.redis.test.ts
docs.ymlзміни документаціїtypecheck, test, docs:build, docs:verify для пакета documentation і деплой на GitHub Pages з main

shared збирається перед перевірками, бо typecheck і тести backend/frontend імпортують shared/dist. Playwright у CI запускає pnpm run preview по зібраному dist, тож frontend build іде перед e2e.

Локально те саме, крім e2e і coverage, проганяє init.sh (install, typecheck, lint, pnpm test, build).

Рецепти ​

Тест сервісу. Створіть <name>-service.spec.ts поруч із сервісом, замокайте barrel репозиторіїв і перевірте Result. Готовий приклад — «Додавання фічі», крок 9.

Тест маршруту / валідації. Візьміть за зразок http-validation.spec.ts: замокайте uWebSockets.js, #app/routes/http-routes.js, #app/routes/ws-routes.js, ws-handlers і execute-httpMiddlewares, оголосіть маршрут через defineRoute зі схемою arktype і перевірте, що на невалідному тілі відповідь 422, а handler не викликається. Для WS — аналогічно ws-validation.spec.ts. Про схеми див. «Валідація».

Запуск:

bash
pnpm --filter backend test                     # усі тести один раз
pnpm --filter backend test:watch               # watch-режим
pnpm --filter backend test csrf-guard          # фільтр за шляхом файлу (аргумент vitest)
pnpm --filter backend exec vitest run -t "422" # фільтр за назвою тесту (через `test -t` pnpm не передає прапорець)
pnpm --filter frontend test:e2e                # Playwright
pnpm --filter frontend test:e2e:csp            # CSP на production-збірці через бекенд
pnpm --filter frontend test:e2e:security       # CSRF і service worker через бекенд з enforce

Прогалини

  • Тестів репозиторіїв поки немає: інфраструктура *.pg.test.ts є, але, крім smoke-тесту, шар даних у тестах мокається.
  • Coverage без порогу і лише по завантажених тестами файлах.

Автор: Олександр Блажейко · blazheiko@gmail.com