From fc554562861e06dc659e75dec13b7722e1adfaa7 Mon Sep 17 00:00:00 2001 From: kamask Date: Mon, 26 Oct 2026 21:17:45 +0300 Subject: [PATCH] test(integration): add integration test harness on angular unit-test builder Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EvmtStx737hu8gXe15e55W --- angular.json | 13 ++++++- docs/testing.md | 25 +++++++++++-- package.json | 1 + src/testing/harness.integration.spec.ts | 47 +++++++++++++++++++++++++ tests/support/clients.ts | 8 +++-- tests/{api => support}/global-setup.ts | 2 +- tsconfig.spec.json | 2 +- tsconfig.tests.json | 2 +- vitest.api.config.mts | 2 +- vitest.integration.config.mts | 11 ++++++ 10 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 src/testing/harness.integration.spec.ts rename tests/{api => support}/global-setup.ts (83%) create mode 100644 vitest.integration.config.mts diff --git a/angular.json b/angular.json index 3d4e906..548c2cb 100644 --- a/angular.json +++ b/angular.json @@ -71,7 +71,18 @@ "defaultConfiguration": "development" }, "test": { - "builder": "@angular/build:unit-test" + "builder": "@angular/build:unit-test", + "options": { + "exclude": ["**/*.integration.spec.ts"] + } + }, + "test-integration": { + "builder": "@angular/build:unit-test", + "options": { + "include": ["**/*.integration.spec.ts"], + "runnerConfig": "vitest.integration.config.mts", + "watch": false + } } } } diff --git a/docs/testing.md b/docs/testing.md index 8149ee6..3c2ad52 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -14,7 +14,28 @@ Команды появляются по мере того, как настраиваются соответствующие стенды. -`pnpm test:api` сначала проверяет типы тестового кода (`tsconfig.tests.json`: `tests/**` и `vitest.api.config.mts`), затем запускает Vitest. +## Стенд API-тестов + +`pnpm test:api` = `tsc -p tsconfig.tests.json && vitest run --config vitest.api.config.mts`: чистый Vitest в Node, без Angular. + +- Сначала проверяются типы тестового кода (`tsconfig.tests.json`: `tests/**` и `vitest.*.config.mts`): сам Vitest типы не проверяет. +- `vitest.api.config.mts`: `include: tests/api/**/*.api.spec.ts`, `globalSetup: tests/support/global-setup.ts`, таймауты 20 с (запас на старт PocketBase внутри теста). +- `global-setup.ts` поднимает один PocketBase на прогон и передаёт тестам `pbUrl`, `pbDataDir`, `pbSuperuser` через `project.provide`; в тестах — `inject('pbUrl')` и т. д. из `vitest`. Типы этих значений — `tests/support/provided-context.d.ts`. +- Тесты самого стенда — `tests/api/harness.api.spec.ts`. Проверка уборки по `SIGINT`/`SIGTERM` запускает фикстуру `tests/support/fixtures/hold-pocketbase.ts` отдельным процессом `node`: она поднимает PocketBase, печатает `{ url, dataDir }` и ждёт сигнала. +- Поэтому `pocketbase-server.ts` выполняется чистым Node без сборки: в нём только импорты `node:*` и стираемый TypeScript (без `enum`, `namespace`, параметров-свойств). + +## Стенд интеграционных тестов + +`pnpm test:integration` = `ng run pchelpcrm:test-integration`: отдельная цель builder'а `@angular/build:unit-test` в `angular.json`. + +- `include: ["**/*.integration.spec.ts"]` в цели `test-integration`; у цели `test` (`pnpm test`) — `exclude` тех же файлов, поэтому unit-тесты не требуют PocketBase. +- `watch: false` в цели: прогон завершается сам; слежение за файлами — разово `pnpm test:integration --watch`. +- `runnerConfig: "vitest.integration.config.mts"` добавляет `globalSetup: tests/support/global-setup.ts` — тот же файл, что у API-тестов, он вызывает `startPocketBase()`. Список файлов в этом конфиге не задаётся: builder его игнорирует. +- В тестах работают `TestBed` и `inject()` Angular, URL — `inject('pbUrl')` из `vitest`, хелперы импортируются из `tests/support/clients.ts`. + +Почему цель builder'а, а не отдельный `vitest.integration.config.ts` с плагином: спеки собираются тем же компилятором Angular и с той же инициализацией TestBed, что и в `pnpm test`, без своего плагина компиляции и его настройки. + +`tests/support/clients.ts` компилируется в двух программах — `tsconfig.tests.json` и `tsconfig.spec.json`. Поэтому в нём нет `node:*` (во второй программе нет типов Node), а `tests/support/provided-context.d.ts` с типами `inject('pbUrl')` перечислен в `include` обоих tsconfig: `references` в `tsconfig.json` программы не объединяют. ## Контрактные тесты репозиториев @@ -45,7 +66,7 @@ API-, integration- и e2e-тесты работают с **отдельным в 4. Ожидание готовности по `GET /api/health`. 5. После прогона процесс останавливается, папка удаляется. -Код запуска один на все уровни: `tests/support/pocketbase-server.ts` (`startPocketBase(): Promise<{ url, dataDir, superuser, stop }>`). В API-тестах URL и суперпользователь доступны через `inject('pbUrl')` и `inject('pbSuperuser')`. +Код запуска один на все уровни: `tests/support/pocketbase-server.ts` (`startPocketBase(): Promise<{ url, dataDir, superuser, stop }>`). Стенд убирает за собой процесс и папку после успешного прогона, после падения (в том числе неудачного старта), по `SIGINT`/`SIGTERM` (Ctrl+C) и при выходе процесса. Предел: `SIGKILL` процесса-владельца перехватить нельзя — тогда PocketBase может продолжить работу, а папка `pchelpcrm-pb-*` остаться в temp. diff --git a/package.json b/package.json index 69b7557..f25a178 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "watch": "ng build --watch --configuration development", "test": "ng test", "test:api": "tsc -p tsconfig.tests.json && vitest run --config vitest.api.config.mts", + "test:integration": "ng run pchelpcrm:test-integration", "lint": "eslint .", "lint:fix": "eslint . --fix", "serve:ssr:pchelpcrm": "node dist/pchelpcrm/server/server.mjs", diff --git a/src/testing/harness.integration.spec.ts b/src/testing/harness.integration.spec.ts new file mode 100644 index 0000000..721ef40 --- /dev/null +++ b/src/testing/harness.integration.spec.ts @@ -0,0 +1,47 @@ +// Временный дымовой тест стенда интеграционных тестов; удалить, когда появятся настоящие *.integration.spec.ts. +import { InjectionToken, Injector } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import type PocketBase from 'pocketbase'; +import { inject } from 'vitest'; +import { asGuest, asSuperuser, asUser, createUser } from '../../tests/support/clients'; + +const PB = new InjectionToken('PB'); + +describe('стенд интеграционных тестов', () => { + it('URL тестового PocketBase доступен и GET /api/health отвечает 200', async () => { + const response = await fetch(`${inject('pbUrl')}/api/health`); + + expect(response.status).toBe(200); + }); + + it('TestBed.inject() работает', () => { + const GREETING = new InjectionToken('GREETING'); + TestBed.configureTestingModule({ providers: [{ provide: GREETING, useValue: 'привет' }] }); + + expect(TestBed.inject(Injector).get(GREETING)).toBe('привет'); + }); + + it('createUser() создаёт пользователя на тестовом PocketBase', async () => { + const user = await createUser(); + + const record = await (await asSuperuser()).collection('users').getOne(user.id); + + expect(record['email']).toBe(user.email); + }); + + it('asUser(user) авторизован именно этим пользователем', async () => { + const user = await createUser(); + + const pb = await asUser(user); + + expect(pb.authStore.record?.id).toBe(user.id); + }); + + it('клиент PocketBase, полученный через DI, обращается к тестовому PocketBase', async () => { + TestBed.configureTestingModule({ providers: [{ provide: PB, useFactory: asGuest }] }); + + const health = await TestBed.inject(PB).health.check(); + + expect(health.code).toBe(200); + }); +}); diff --git a/tests/support/clients.ts b/tests/support/clients.ts index ae8308c..00ceba6 100644 --- a/tests/support/clients.ts +++ b/tests/support/clients.ts @@ -1,5 +1,5 @@ // PocketBase SDK clients for tests; each helper returns its own instance with an in-memory auth store. -import { randomUUID } from 'node:crypto'; +// Используется и из src/**/*.integration.spec.ts (tsconfig.spec.json), где нет типов Node: поэтому глобальный crypto вместо node:crypto. import PocketBase, { BaseAuthStore } from 'pocketbase'; import { inject } from 'vitest'; @@ -21,7 +21,11 @@ export async function asSuperuser(): Promise { } export async function createUser(overrides: Record = {}): Promise { - const data = { email: `user-${randomUUID()}@test.local`, password: randomUUID(), ...overrides }; + const data = { + email: `user-${crypto.randomUUID()}@test.local`, + password: crypto.randomUUID(), + ...overrides, + }; const record = await ( await asSuperuser() ) diff --git a/tests/api/global-setup.ts b/tests/support/global-setup.ts similarity index 83% rename from tests/api/global-setup.ts rename to tests/support/global-setup.ts index 2de6170..d8a7ff5 100644 --- a/tests/api/global-setup.ts +++ b/tests/support/global-setup.ts @@ -1,5 +1,5 @@ import type { TestProject } from 'vitest/node'; -import { startPocketBase } from '../support/pocketbase-server'; +import { startPocketBase } from './pocketbase-server'; export default async function setup(project: TestProject) { const { url, dataDir, superuser, stop } = await startPocketBase(); diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 23d738d..a0057f8 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -5,5 +5,5 @@ "compilerOptions": { "types": ["vitest/globals"] }, - "include": ["src/**/*.d.ts", "src/**/*.spec.ts"] + "include": ["src/**/*.d.ts", "src/**/*.spec.ts", "tests/support/provided-context.d.ts"] } diff --git a/tsconfig.tests.json b/tsconfig.tests.json index 293b7e9..344ffee 100644 --- a/tsconfig.tests.json +++ b/tsconfig.tests.json @@ -8,5 +8,5 @@ "types": ["node"] }, "files": [], - "include": ["tests/**/*.ts", "vitest.api.config.mts"] + "include": ["tests/**/*.ts", "vitest.api.config.mts", "vitest.integration.config.mts"] } diff --git a/vitest.api.config.mts b/vitest.api.config.mts index 3eccfa2..de552d5 100644 --- a/vitest.api.config.mts +++ b/vitest.api.config.mts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { include: ['tests/api/**/*.api.spec.ts'], - globalSetup: ['tests/api/global-setup.ts'], + globalSetup: ['tests/support/global-setup.ts'], // Headroom for starting a temporary PocketBase inside a test. testTimeout: 20_000, hookTimeout: 20_000, diff --git a/vitest.integration.config.mts b/vitest.integration.config.mts new file mode 100644 index 0000000..017439a --- /dev/null +++ b/vitest.integration.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +// Дополняет конфигурацию builder'а @angular/build:unit-test (цель test-integration в angular.json). +// Список файлов задаётся в angular.json: include из этого файла builder игнорирует. +export default defineConfig({ + test: { + globalSetup: ['tests/support/global-setup.ts'], + testTimeout: 20_000, + hookTimeout: 20_000, + }, +});