Cookbook — Запустить тесты Payment Manager¶
Summary. Как запускать тесты PM локально: полный прогон, фильтрация по файлу/имени, watch-режим, integration, lint. Раскладка
test/, шаблон нового теста, известные подсказки по troubleshooting.
Что делаем¶
Запускаем тесты Payment Manager. Тест-runner — Vitest (vitest run под капотом npm test). Конфиг — vitest.config.ts (корень проекта). Все тесты исполняются с фиксированным набором env-переменных (NODE_ENV=test, тестовые DATABASE_URL/REDIS_URL/TB_ADDRESS, тестовые SERVICE_SECRETS).
Тесты делятся на:
- Юнит-тесты — изолированно проверяют функцию/хендлер с заглушками (
test/<module>/<file>.test.ts). - Интеграционные — поднимают Fastify app через
buildTestApp()и реальные сервисы (PostgreSQL/Redis/TigerBeetle) в Docker — в каталогеtest/integration/.
Предусловия¶
- Установлены зависимости:
npm install. - Для integration тестов в фоне подняты PostgreSQL, Redis и TigerBeetle (см. ./run-locally.md). Юнит-тесты, использующие моки, можно запускать без сервисов — но любой тест, открывающий реальный коннект, упадёт без них.
- Локальная БД
testдоступна поpostgres://test:test@localhost:5432/test(значение изvitest.config.ts).
Шаги¶
1. Все тесты разом¶
Под капотом — vitest run (один проход, без watch). Это то, что гоняет CI. Используй перед коммитом / перед PR.
2. Фильтр по файлу¶
Можно указать несколько путей через пробел или glob:
3. Фильтр по имени теста (-t)¶
Полезно, когда нужен один конкретный сценарий из большого файла.
4. Watch mode¶
Эквивалентно npx vitest (без run). Перезапускает только затронутые тесты при сохранении файла. Внутри REPL: p — фильтр по пути, t — по имени, a — прогнать всё, q — выйти.
5. Coverage¶
В package.json отдельного скрипта нет, но установлен @vitest/coverage-v8 — можно запустить напрямую:
Отчёт ложится в ./coverage/. Открыть coverage/index.html в браузере.
6. Lint (бонус)¶
Отдельного lint-скрипта в package.json сейчас нет; TypeScript-проверка делается через npm run build (т.е. tsc). Если в CI собирается — добавляй типовые ошибки в чек-лист перед PR:
Раскладка test/¶
test/
├── helpers/ # общие билдеры: buildTestApp(), signRequest(), seedDB()
│ ├── app.ts # buildTestApp() + HMAC helper для интеграционных тестов
│ └── db.ts # фабрики для вставки тестовых данных
├── mocks/ # in-memory моки внешних систем
│ └── ipps-mock-server.ts # фейковый IPPS, поднимается внутри тестов
├── integration/ # интеграционные тесты на реальном app + сервисах
│ ├── invoice-flow.test.ts
│ └── notifications.test.ts
├── intent/ # юнит-тесты intent-pipeline (handler, confirm, cancel, saga)
├── limits/ # лимит-engine + policy evaluator
├── policies/ # step-up policy routes/CRUD
├── rule-engine/ # fee/route rules
├── psp/ # IPPS driver, psp-worker
├── ledger/ # TigerBeetle wrappers, settlement-writer
├── channels/ # single/two-phase каналы (INTERNAL_P2P, MERCHANT_INVOICE …)
├── jobs/ # OutboxWorker, expirator, reconciler
├── workers/ # фоновые воркеры (notify, saga-runner)
├── accounts/ # резолвер аккаунтов, tb_account_map
├── operation-types/ # operation-registry, parseBody/resolveAccounts
├── admin/ # admin-маршруты
├── shared/ # утилиты (request-context, idempotency и т.п.)
├── hmac.test.ts # глобальная проверка HMAC middleware
├── health.test.ts # /healthz, /readyz
├── config.test.ts # загрузка env / валидация
└── adminPlugin.test.ts # admin-плагин Fastify
Правила именования:
- Файл:
<тестируемая-сущность>.test.ts. Не.spec.ts. - Папка совпадает с модулем в
src/. Если такого модуля нет — стоп, скорее всего тест неуместен в этом каталоге. - Helpers — только в
test/helpers/. Никаких re-export-цепочек из других тестов. - Моки внешних адаптеров (IPPS, QP, Notifications) — в
test/mocks/.
Как добавить новый тест¶
Пример: пишем юнит-тест для функции validateInvoiceAmount из src/invoice/validate.ts.
- Создай файл
test/invoice/validate.test.ts(папкуinvoice/создай, если её нет). - Минимальный скелет:
/**
* Юнит-тесты validateInvoiceAmount: проверяем граничные значения min/max
* и реакцию на отрицательные суммы.
*/
import { describe, it, expect } from 'vitest'
import { validateInvoiceAmount } from '../../src/invoice/validate.js'
describe('validateInvoiceAmount', () => {
it('принимает сумму в допустимом диапазоне', () => {
expect(() => validateInvoiceAmount(10000n)).not.toThrow()
})
it('отбрасывает отрицательную сумму', () => {
expect(() => validateInvoiceAmount(-1n)).toThrow(/non-negative/)
})
})
- Для интеграционного теста подними app через helper:
import { buildTestApp, signRequest } from '../helpers/app.js'
const app = await buildTestApp()
const res = await app.inject({
method: 'POST',
url: '/intents',
headers: signRequest(body),
payload: body,
})
expect(res.statusCode).toBe(200)
- Прогон одного файла:
- Перед PR — полный прогон
npm test(CI делает то же самое).
Troubleshooting и известные подсказки¶
ECONNREFUSED 127.0.0.1:5432/:6379/:3000— не подняты Postgres / Redis / TigerBeetle. Запусти локальный стек (см. ./run-locally.md) либо запускай только юнит-тесты конкретного модуля без реальных коннектов.- HMAC
401 invalid signatureв интеграционных тестах — проверь, что используешьsignRequest()изtest/helpers/app.ts, а не самописную подпись. Секреты заданы вvitest.config.ts(полеSERVICE_SECRETS) — это тестовые значения, в проде они другие. tx_historyостался от прошлого прогона — интеграционные тесты должны чистить state вbeforeEachчерез хелперы изtest/helpers/db.ts. Если флак — добавь truncate соответствующихpm.*таблиц.- Висит навсегда — обычно забыт
await app.close()послеbuildTestApp(). Vitest ждёт закрытия открытых хендлов. - Тест проходит локально, падает в CI — проверь, что не зависишь от текущего времени без
vi.useFakeTimers()и от порядка тестов в файле. - Канал в тестах должен называться
ADMIN(для админ-операций), а не выдуманные синонимы — это инвариант passport, не меняй его «по вкусу».
Связанные документы¶
- ../testing/overview.md — стратегия тестирования, что юнит / что интеграция.
- ./run-locally.md — поднять Postgres + Redis + TigerBeetle локально.
- ./add-operation-type.md — чек-лист тестов при добавлении operationType.
- PASSPORT.md — контрактные инварианты, которые тесты обязаны защищать.