# Testing Strategy This document outlines the testing strategy for the Annos service, following Domain-Driven Design (DDD) and Layered Architecture principles. ## Goals - Ensure correctness and reliability of the application. - Provide fast feedback during development. - Facilitate refactoring and maintenance. - Verify integration between different parts of the system. ## Testing Levels We will employ a multi-layered testing approach, focusing tests at the appropriate level of the architecture: 1. **Unit Tests:** - **Focus:** Individual classes, methods, or functions in isolation. Primarily target Domain Layer objects (Aggregates, Value Objects, Domain Services) and utility functions. - **Scope:** Test business logic, validation rules, state transitions, and calculations within a single unit. - **Dependencies:** Mocked or stubbed. No external dependencies (database, network, file system). - **Tools:** Jest. - **Location:** `tests//domain/**` (e.g., `tests/annotations/domain/value-objects/AnnotationValue.test.ts`) 2. **Integration Tests:** - **Focus:** Interactions between components within a layer or across adjacent layers. Primarily target Application Layer Use Cases and Infrastructure Layer components (Repositories, Mappers). - **Scope:** - _Application Layer:_ Verify use cases correctly orchestrate domain objects and interact with repositories (using mocked repositories). - _Infrastructure Layer:_ Verify repositories correctly interact with the database (using a test database or in-memory alternatives if feasible), or mappers correctly transform data. - **Dependencies:** May involve mocked components (e.g., mocking a repository for a use case test) or real infrastructure components connected to a test environment (e.g., testing a repository against a test database). - **Tools:** Jest, Test Database (e.g., Dockerized Postgres), potentially Supertest for API endpoint integration. - **Location:** `tests//application/**`, `tests//infrastructure/**` (e.g., `tests/annotations/application/use-cases/CreateAnnotationUseCase.test.ts`, `tests/annotations/infrastructure/persistence/AnnotationRepository.integration.test.ts`) 3. **End-to-End (E2E) Tests:** - **Focus:** Simulating real user scenarios through the entire system, typically via the API or UI. - **Scope:** Verify complete workflows, from request initiation (e.g., HTTP request) to response validation, including interactions with the database and potentially external services. - **Dependencies:** Requires a fully running instance of the application and its dependencies (database, etc.) in a dedicated test environment. - **Tools:** Supertest (for API testing), potentially Playwright or Cypress if a UI is involved. - **Location:** `tests/e2e/**` ## Guiding Principles - **Test Pyramid:** Emphasize a larger number of fast unit tests, a moderate number of integration tests, and fewer, more comprehensive E2E tests. - **Isolate Layers:** Test domain logic independently of application and infrastructure concerns. Test application logic with mocked infrastructure. Test infrastructure against real (test) dependencies. - **Mocking:** Use mocking judiciously, primarily at the boundaries between layers (e.g., mocking repositories in use case tests). Avoid excessive mocking within a single unit. - **Test Data:** Use realistic and clearly defined test data. Consider factories or builders for creating complex objects. - **CI/CD:** Integrate tests into the Continuous Integration pipeline to ensure code quality and prevent regressions. ## Test Runner & Environment - **Runner:** Jest - **Configuration:** `jest.config.js` (to be created if needed for more complex setup). - **Execution:** `npm test` ## Current Focus Initially, we will focus on: - Unit tests for critical Domain Layer Value Objects and Aggregates. - Integration tests for Application Layer Use Cases, mocking the repository layer. ## Testing Repository Implementations Repository implementations like `DrizzleAnnotationFieldRepository` and `DrizzleAnnotationTemplateRepository` require special consideration as they interact with databases: ### Approach 1: In-memory Database (Recommended for Unit Tests) For fast, isolated tests of repository implementations: 1. **Use SQLite in-memory database:** ```typescript import { drizzle } from 'drizzle-orm/better-sqlite3'; import Database from 'better-sqlite3'; import { migrate } from 'drizzle-orm/better-sqlite3/migrator'; // Setup in-memory SQLite for tests const sqlite = new Database(':memory:'); const db = drizzle(sqlite); // Apply migrations or create schema migrate(db, { migrationsFolder: './drizzle' }); // Create repository with in-memory database const repo = new DrizzleAnnotationFieldRepository(db); ``` 2. **Benefits:** - Fast execution - no network or disk I/O - Isolated - each test gets a fresh database - No external dependencies or setup required 3. **Limitations:** - SQLite dialect differences from PostgreSQL - Some PostgreSQL-specific features won't be testable ### Approach 2: Test Containers (For Integration Tests) For more realistic tests that verify PostgreSQL compatibility: 1. **Use testcontainers-node to spin up a PostgreSQL container:** ```typescript import { PostgreSqlContainer } from 'testcontainers'; import postgres from 'postgres'; import { drizzle } from 'drizzle-orm/postgres-js'; // In beforeAll hook const container = await new PostgreSqlContainer().start(); const connectionString = container.getConnectionUri(); const sql = postgres(connectionString); const db = drizzle(sql); // In afterAll hook await container.stop(); ``` 2. **Benefits:** - Tests against actual PostgreSQL - Verifies dialect-specific features - Isolated from development/production databases 3. **Drawbacks:** - Slower test execution - Requires Docker ### Approach 3: Mock Database Client (For Pure Unit Tests) For pure unit tests focusing on repository logic: 1. **Mock the database client:** ```typescript const mockDb = { select: jest.fn().mockReturnThis(), from: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), limit: jest.fn().mockReturnValue([ { /* mock data */ }, ]), insert: jest.fn().mockReturnThis(), values: jest.fn().mockReturnThis(), onConflictDoUpdate: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockReturnThis(), }; const repo = new DrizzleAnnotationFieldRepository(mockDb as any); ``` 2. **Benefits:** - Fastest execution - No database dependencies - Tests repository logic in isolation 3. **Drawbacks:** - Doesn't test actual SQL generation - Requires complex mocking setup - Lower confidence in database interaction ### Recommended Approach Use a combination: 1. **Unit tests with SQLite in-memory** for fast feedback during development 2. **Integration tests with test containers** for critical paths and PostgreSQL-specific features 3. **End-to-end tests** that verify the entire stack works together This provides a balance of speed, isolation, and confidence in your repository implementations.