7.3 KiB
Тестирование
Разработка ведётся по TDD: тесты пишутся до реализации.
Уровни тестов
| Уровень | Что проверяет | Где лежит | Команда |
|---|---|---|---|
| API (e2e бэкенда) | схема, миграции, правила доступа, изоляция пользователей, хуки PocketBase | tests/api/**/*.api.spec.ts |
pnpm test:api |
| Unit | доменные функции, компоненты и сервисы с фейками вместо PocketBase | рядом с кодом, src/**/*.spec.ts |
pnpm test |
| Integration | PocketBase-реализации репозиториев и сервисов на настоящем PocketBase | рядом с кодом, src/**/*.integration.spec.ts |
pnpm test:integration |
| E2E | пользовательские сценарии в браузере, SSR, доступность (axe) | tests/e2e/**/*.e2e.ts |
pnpm e2e |
| E2E prod | сценарии на production-сборке, включая изоляцию данных при SSR | tests/e2e/ (проект ssr-prod) |
pnpm e2e:prod |
Команды появляются по мере того, как настраиваются соответствующие стенды. Устройство каждого стенда описано в guide/tests/:
- Стенд API-тестов
- Стенд интеграционных тестов
- Стенд e2e
- Тесты конфигов (ESLint, dev-прокси, Playwright)
Контрактные тесты репозиториев
У каждого репозитория две реализации: PocketBase (data/) и фейк в памяти (testing/). Чтобы фейк не расходился с реальностью, поведение описывается один раз:
// tags/testing/tag-repository.contract.ts
export function describeTagRepositoryContract(
setup: () => Promise<{ repoA: TagRepository; repoB: TagRepository }>,
) {
describe('контракт TagRepository', () => {
it('не показывает пользователю B метки пользователя A', async () => {
/* ... */
});
});
}
и вызывается дважды: в in-memory-tag-repository.spec.ts (быстро, в pnpm test) и в pocketbase-tag-repository.integration.spec.ts (на тестовом PocketBase, в pnpm test:integration).
Так же устроен контракт сервиса AuthService: auth/testing/auth-service.contract.ts. Файл контракта не *.spec.ts, поэтому describe/it/expect импортируются из vitest явно.
Тестовый PocketBase
API-, integration- и e2e-тесты работают с отдельным временным экземпляром PocketBase, а не с pocketbase/pb_data:
- Создаётся временная папка данных (в системной temp-папке).
- Создаётся суперпользователь (
pocketbase superuser upsert) со сгенерированными email и паролем: инстанс одноразовый, поэтому.envтестам не нужен. - Запускается
pocketbase serveна свободном порту127.0.0.1(или на заданном: e2e — 8190) с--dir <временная папка> --migrationsDir pocketbase/pb_migrations --hooksDir pocketbase/pb_hooks --automigrate=false; миграции применяются при старте. - Ожидание готовности по
GET /api/health. - После прогона процесс останавливается, папка удаляется.
Код запуска один на все уровни: tests/support/pocketbase-server.ts (startPocketBase({ port? }): Promise<{ url, dataDir, superuser, stop }>).
Стенд убирает за собой процесс и папку после успешного прогона, после падения (в том числе неудачного старта), по SIGINT/SIGTERM (Ctrl+C) и при выходе процесса. Предел: SIGKILL процесса-владельца перехватить нельзя — тогда PocketBase может продолжить работу, а папка pchelpcrm-pb-* остаться в temp.
Правила написания тестов
- Каждый тест создаёт своих пользователей с уникальными email (
user-<случайная строка>@test.local). Тесты не зависят друг от друга и от порядка, поэтому могут идти параллельно и не нуждаются в очистке. - Хелперы (
tests/support/):createUser(),asUser(user),asGuest(),asSuperuser()возвращают готовый экземпляр PocketBase SDK. - Изоляцию проверяем всегда двумя пользователями: A создаёт данные, B пытается их прочитать, найти через list/filter, изменить, удалить, сослаться на них. Плюс гость.
- Тест проверяет поведение, а не реализацию: статус ответа, тело, состояние после операции (перечитать суперпользователем).
- Unit-тесты компонентов используют фейки из
<feature>/testing/и не обращаются к сети. - Компонентные тесты ищут элементы по ролям и подписям (как пользователь и скринридер), а не по CSS-классам.
- E2E-тесты: каждый сценарий регистрирует своего пользователя через UI или API; на ключевых страницах выполняется проверка axe (
@axe-core/playwright) без нарушений. - Названия тестов на русском и описывают поведение:
it('не отдаёт гостю чужую метку'). - Пока реализации нет, тесты должны компилироваться: для этого создаются минимальные заглушки контрактов (типы, абстрактные классы, методы с
throw new Error('Not implemented')). Новые тесты падают из-за невыполненных ожиданий, а не из-за ошибок импорта или стенда.