test(integration): add integration test harness on angular unit-test builder

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvmtStx737hu8gXe15e55W
This commit is contained in:
kamask
2026-10-26 21:17:45 +03:00
co-authored by Claude Opus 5.5
parent 1109f68e11
commit fc55456286
10 changed files with 104 additions and 9 deletions
+12 -1
View File
@@ -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
}
}
}
}
+23 -2
View File
@@ -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.
+1
View File
@@ -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",
+47
View File
@@ -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<PocketBase>('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<string>('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);
});
});
+6 -2
View File
@@ -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<PocketBase> {
}
export async function createUser(overrides: Record<string, unknown> = {}): Promise<TestUser> {
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()
)
@@ -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();
+1 -1
View File
@@ -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"]
}
+1 -1
View File
@@ -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"]
}
+1 -1
View File
@@ -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,
+11
View File
@@ -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,
},
});