# Life Management System — Testing Strategy ## Philosophy Test pyramid: more unit tests, fewer integration tests, fewest e2e tests. Focus testing effort where bugs are most likely and most costly: service business logic, encrypted field handling, and API contract compliance. ``` ╱╲ ╱ ╲ E2E Tests (few, critical paths only) ╱────╲ supertest + WebSocket client ╱ ╲ ╱ ╲ Integration Tests (moderate, per-controller) ╱──────────╲ Test database + real queries ╱ ╲ ╱ ╲ Unit Tests (many, per-service) ╱────────────────╲ Mocked repositories + pure logic ``` --- ## Test Stack | Layer | Tool | Package | |-------|------|---------| | Test runner | Vitest | `vitest` | | Backend unit | Vitest + NestJS testing | `@nestjs/testing` | | Backend integration | Vitest + supertest | `supertest` | | Backend e2e | Vitest + supertest + socket.io-client | `supertest`, `socket.io-client` | | Frontend unit | Vitest + Testing Library | `@testing-library/react` | | Frontend integration | Vitest + MSW | `msw` | | Coverage | v8 (via Vitest) | built-in | --- ## Backend Unit Tests ### Scope Test service-layer business logic with mocked TypeORM repositories. No database connection. ### Pattern ```typescript // modules/tasks/tasks.service.spec.ts describe('TasksService', () => { let service: TasksService; let repository: MockType>; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ TasksService, { provide: getRepositoryToken(Task), useFactory: repositoryMockFactory, }, { provide: EventEmitter2, useValue: { emit: vi.fn() }, }, ], }).compile(); service = module.get(TasksService); repository = module.get(getRepositoryToken(Task)); }); describe('create', () => { it('creates a task with default values', async () => { const dto: CreateTaskDto = { domainId: 'uuid-1', title: 'Test task', }; repository.create.mockReturnValue({ ...dto, id: 'uuid-new' }); repository.save.mockResolvedValue({ ...dto, id: 'uuid-new', status: 'todo' }); const result = await service.create(dto); expect(result.status).toBe('todo'); expect(repository.save).toHaveBeenCalled(); }); it('auto-flags quick wins when estimated under 5 minutes', async () => { const dto: CreateTaskDto = { domainId: 'uuid-1', title: 'Quick task', estimatedMinutes: 3, }; repository.create.mockReturnValue({ ...dto, isQuickWin: true }); repository.save.mockResolvedValue({ ...dto, isQuickWin: true }); const result = await service.create(dto); expect(result.isQuickWin).toBe(true); }); }); describe('updateStatus', () => { it('emits TaskCompletedEvent when status changes to done', async () => { const task = { id: 'uuid-1', domainId: 'uuid-d', status: 'todo' }; repository.findOne.mockResolvedValue(task); repository.save.mockResolvedValue({ ...task, status: 'done' }); await service.updateStatus('uuid-1', { status: 'done' }); expect(eventEmitter.emit).toHaveBeenCalledWith( 'task.completed', expect.any(TaskCompletedEvent), ); }); }); }); ``` ### What to Unit Test - **Service methods:** Create, update, delete logic and edge cases - **Entity validation:** DTO constraint validation (class-validator) - **Business rules:** Quick win auto-flagging, streak calculation logic, overdue detection, suggestion scoring - **Event emission:** Correct events emitted on state transitions - **Error handling:** Not-found, validation failures, constraint violations --- ## Backend Integration Tests ### Scope Test controller endpoints with a real test database. Validates request/response shapes, query filtering, pagination, and encrypted field round-trips. ### Pattern ```typescript // modules/tasks/tasks.controller.spec.ts describe('TasksController (integration)', () => { let app: INestApplication; beforeAll(async () => { const module = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(ConfigService) .useValue(testConfigService) .compile(); app = module.createNestApplication(); applyGlobalPipes(app); await app.init(); }); afterAll(async () => { await app.close(); }); beforeEach(async () => { await truncateAllTables(app); await seedTestDomains(app); }); describe('GET /api/tasks', () => { it('returns paginated tasks filtered by domain', async () => { await createTestTask(app, { domainId: domain1.id, title: 'Task 1' }); await createTestTask(app, { domainId: domain2.id, title: 'Task 2' }); const response = await request(app.getHttpServer()) .get(`/api/tasks?domainId=${domain1.id}`) .expect(200); expect(response.body.data).toHaveLength(1); expect(response.body.data[0].title).toBe('Task 1'); expect(response.body.meta.total).toBe(1); }); it('filters by multiple statuses', async () => { await createTestTask(app, { status: 'todo' }); await createTestTask(app, { status: 'in_progress' }); await createTestTask(app, { status: 'done' }); const response = await request(app.getHttpServer()) .get('/api/tasks?status=todo,in_progress') .expect(200); expect(response.body.data).toHaveLength(2); }); }); describe('POST /api/tasks', () => { it('validates required fields', async () => { const response = await request(app.getHttpServer()) .post('/api/tasks') .send({}) .expect(400); expect(response.body.details).toContainEqual( expect.objectContaining({ field: 'title' }), ); }); }); }); ``` ### Encrypted Field Round-Trip Tests ```typescript describe('IncomeController (encrypted)', () => { it('encrypts amount on write and decrypts on read', async () => { const createResponse = await request(app.getHttpServer()) .post('/api/income') .send({ domainId: domain.id, date: '2026-02-24', amount: '150.00', sourceType: 'session', }) .expect(201); // Verify stored as encrypted in DB const raw = await queryRunner.query( 'SELECT amount_encrypted FROM income_entries WHERE id = $1', [createResponse.body.id], ); expect(raw[0].amount_encrypted).toBeInstanceOf(Buffer); // bytea, not plaintext // Verify decrypted on read const readResponse = await request(app.getHttpServer()) .get(`/api/income`) .expect(200); expect(readResponse.body.data[0].amount).toBe('150.00'); }); }); ``` --- ## Backend E2E Tests ### Scope Full API workflow tests and WebSocket event tests. Test complete user flows spanning multiple endpoints. ### API Workflow Example ```typescript describe('Daily workflow (e2e)', () => { it('completes a full daily cycle', async () => { // 1. Set morning plan await request(app.getHttpServer()) .put('/api/scheduling/daily-plan/2026-02-24') .send({ energyLevel: 'high', moodMorning: 4, morningIntention: 'Focus on sprint' }) .expect(200); // 2. Create tasks const task = await createTestTask(app, { domainId: softwareDomain.id, title: 'Implement auth guards', dueDate: '2026-02-24', }); // 3. Get today view const today = await request(app.getHttpServer()) .get('/api/today') .expect(200); expect(today.body.dailyPlan.energyLevel).toBe('high'); expect(today.body.priorityTasks).toContainEqual( expect.objectContaining({ title: 'Implement auth guards' }), ); // 4. Complete task await request(app.getHttpServer()) .patch(`/api/tasks/${task.id}/status`) .send({ status: 'done', actualMinutes: 45 }) .expect(200); // 5. Verify today updated const todayAfter = await request(app.getHttpServer()) .get('/api/today') .expect(200); expect(todayAfter.body.completedToday).toBeGreaterThan(today.body.completedToday); }); }); ``` ### WebSocket E2E Example ```typescript describe('Chat WebSocket (e2e)', () => { let socket: Socket; beforeEach((done) => { socket = io(`http://localhost:${port}/chat`); socket.on('connect', done); }); afterEach(() => { socket.disconnect(); }); it('streams message chunks on send_message', (done) => { const chunks: string[] = []; socket.on('message_start', ({ messageId }) => { expect(messageId).toBeDefined(); }); socket.on('message_chunk', ({ chunk }) => { chunks.push(chunk); }); socket.on('message_end', ({ messageId, tokenCount }) => { expect(chunks.length).toBeGreaterThan(0); expect(tokenCount).toBeGreaterThan(0); done(); }); socket.emit('send_message', { conversationId: testConversation.id, content: 'What are my overdue tasks?', }); }); }); ``` --- ## Frontend Unit Tests ### Scope Component render tests and hook logic tests. No real API calls — everything mocked. ### Pattern ```typescript // pages/tasks/TaskListItem.test.tsx describe('TaskListItem', () => { it('renders task title and badges', () => { const task = createMockTask({ title: 'Design review', priority: 'high', energyLevel: 'high', status: 'todo', }); render(); expect(screen.getByText('Design review')).toBeInTheDocument(); expect(screen.getByText('high')).toBeInTheDocument(); // priority badge }); it('calls onStatusChange when checkbox clicked', async () => { const onStatusChange = vi.fn(); const task = createMockTask({ status: 'todo' }); render(); await userEvent.click(screen.getByRole('checkbox')); expect(onStatusChange).toHaveBeenCalledWith('done'); }); }); ``` ### Hook Tests ```typescript // api/tasks.test.ts describe('useTasks', () => { it('fetches tasks with filters', async () => { const wrapper = createQueryClientWrapper(); server.use( rest.get('/api/tasks', (req, res, ctx) => { expect(req.url.searchParams.get('domainId')).toBe('uuid-1'); return res(ctx.json({ data: [mockTask], meta: { page: 1, limit: 20, total: 1, totalPages: 1 } })); }), ); const { result } = renderHook( () => useTasks({ domainId: 'uuid-1' }), { wrapper }, ); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data.data).toHaveLength(1); }); }); ``` --- ## Frontend Integration Tests ### Scope Page-level tests with MSW (Mock Service Worker) for API mocking. Test complete page rendering and user interactions. ### Pattern ```typescript // pages/tasks/TasksPage.test.tsx describe('TasksPage', () => { beforeEach(() => { server.use( rest.get('/api/tasks', (req, res, ctx) => { return res(ctx.json({ data: [ createMockTask({ title: 'Overdue task', dueDate: '2026-02-20', status: 'todo' }), createMockTask({ title: 'Today task', dueDate: '2026-02-24', status: 'todo' }), ], meta: { page: 1, limit: 20, total: 2, totalPages: 1 }, })); }), rest.get('/api/domains', (req, res, ctx) => { return res(ctx.json(mockDomains)); }), ); }); it('renders tasks grouped by urgency', async () => { render(, { wrapper: AppProviders }); await waitFor(() => { expect(screen.getByText('Overdue task')).toBeInTheDocument(); expect(screen.getByText('Today task')).toBeInTheDocument(); }); // Overdue group appears before today group const overdue = screen.getByText('OVERDUE'); const today = screen.getByText('DUE TODAY'); expect(overdue.compareDocumentPosition(today)) .toBe(Node.DOCUMENT_POSITION_FOLLOWING); }); it('filters tasks when filter changes', async () => { render(, { wrapper: AppProviders }); await userEvent.click(screen.getByText('Quick wins only')); await waitFor(() => { // MSW handler checks isQuickWin param expect(screen.getByText('Quick tasks')).toBeInTheDocument(); }); }); }); ``` --- ## Test Database ### Configuration Separate PostgreSQL database for tests, created via Docker Compose: ```yaml # docker-compose.test.yml (extends docker-compose.yml) services: postgres-test: image: postgres:16-alpine ports: - "25471:5432" environment: POSTGRES_DB: life_manager_test POSTGRES_USER: lilith POSTGRES_PASSWORD: test_password tmpfs: - /var/lib/postgresql/data # RAM-backed for speed ``` ### Lifecycle ``` 1. Before test suite: Run all migrations on test DB 2. Before each test: Truncate all tables (preserve schema) 3. Each test: Seed needed data → run assertions → data discarded 4. After test suite: Drop test DB (or leave for debugging) ``` ### Truncation Helper ```typescript export async function truncateAllTables(app: INestApplication) { const dataSource = app.get(DataSource); const tables = dataSource.entityMetadatas.map(e => `"${e.tableName}"`).join(', '); await dataSource.query(`TRUNCATE TABLE ${tables} CASCADE`); } ``` --- ## Coverage Targets | Layer | Target | Rationale | |-------|--------|-----------| | Backend services | 80% | Core business logic — highest value | | Backend controllers | 70% | Request/response contracts | | Frontend components | 60% | UI rendering correctness | | Frontend hooks | 70% | Data fetching logic | | E2E workflows | Key paths | Critical user journeys | ### Coverage Commands ```bash # Run all tests with coverage pnpm test -- --coverage # Backend only pnpm --filter backend-api test -- --coverage # Frontend only pnpm --filter frontend test -- --coverage ``` --- ## CI Pipeline ```bash # turbo runs tests in dependency order: shared → backend-api → frontend turbo test # Pipeline definition (turbo.json): { "pipeline": { "build": { "dependsOn": ["^build"] }, "typecheck": { "dependsOn": ["^build"] }, "test": { "dependsOn": ["build"], "outputs": ["coverage/**"] }, "test:e2e": { "dependsOn": ["build"] } } } ``` ### CI Steps 1. `pnpm install --frozen-lockfile` 2. `docker compose -f docker-compose.test.yml up -d` (test DB) 3. `turbo build` (compile shared → backend → frontend) 4. `turbo typecheck` (TypeScript checks) 5. `turbo test` (unit + integration tests with coverage) 6. `turbo test:e2e` (e2e tests against test DB) 7. Coverage report upload