A social knowledge tool for researchers built on ATProto
1# Testing Strategy
2
3This document outlines the testing strategy for the Annos service, following Domain-Driven Design (DDD) and Layered Architecture principles.
4
5## Goals
6
7- Ensure correctness and reliability of the application.
8- Provide fast feedback during development.
9- Facilitate refactoring and maintenance.
10- Verify integration between different parts of the system.
11
12## Testing Levels
13
14We will employ a multi-layered testing approach, focusing tests at the appropriate level of the architecture:
15
161. **Unit Tests:**
17 - **Focus:** Individual classes, methods, or functions in isolation. Primarily target Domain Layer objects (Aggregates, Value Objects, Domain Services) and utility functions.
18 - **Scope:** Test business logic, validation rules, state transitions, and calculations within a single unit.
19 - **Dependencies:** Mocked or stubbed. No external dependencies (database, network, file system).
20 - **Tools:** Jest.
21 - **Location:** `tests/<bounded-context>/domain/**` (e.g., `tests/annotations/domain/value-objects/AnnotationValue.test.ts`)
22
232. **Integration Tests:**
24 - **Focus:** Interactions between components within a layer or across adjacent layers. Primarily target Application Layer Use Cases and Infrastructure Layer components (Repositories, Mappers).
25 - **Scope:**
26 - _Application Layer:_ Verify use cases correctly orchestrate domain objects and interact with repositories (using mocked repositories).
27 - _Infrastructure Layer:_ Verify repositories correctly interact with the database (using a test database or in-memory alternatives if feasible), or mappers correctly transform data.
28 - **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).
29 - **Tools:** Jest, Test Database (e.g., Dockerized Postgres), potentially Supertest for API endpoint integration.
30 - **Location:** `tests/<bounded-context>/application/**`, `tests/<bounded-context>/infrastructure/**` (e.g., `tests/annotations/application/use-cases/CreateAnnotationUseCase.test.ts`, `tests/annotations/infrastructure/persistence/AnnotationRepository.integration.test.ts`)
31
323. **End-to-End (E2E) Tests:**
33 - **Focus:** Simulating real user scenarios through the entire system, typically via the API or UI.
34 - **Scope:** Verify complete workflows, from request initiation (e.g., HTTP request) to response validation, including interactions with the database and potentially external services.
35 - **Dependencies:** Requires a fully running instance of the application and its dependencies (database, etc.) in a dedicated test environment.
36 - **Tools:** Supertest (for API testing), potentially Playwright or Cypress if a UI is involved.
37 - **Location:** `tests/e2e/**`
38
39## Guiding Principles
40
41- **Test Pyramid:** Emphasize a larger number of fast unit tests, a moderate number of integration tests, and fewer, more comprehensive E2E tests.
42- **Isolate Layers:** Test domain logic independently of application and infrastructure concerns. Test application logic with mocked infrastructure. Test infrastructure against real (test) dependencies.
43- **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.
44- **Test Data:** Use realistic and clearly defined test data. Consider factories or builders for creating complex objects.
45- **CI/CD:** Integrate tests into the Continuous Integration pipeline to ensure code quality and prevent regressions.
46
47## Test Runner & Environment
48
49- **Runner:** Jest
50- **Configuration:** `jest.config.js` (to be created if needed for more complex setup).
51- **Execution:** `npm test`
52
53## Current Focus
54
55Initially, we will focus on:
56
57- Unit tests for critical Domain Layer Value Objects and Aggregates.
58- Integration tests for Application Layer Use Cases, mocking the repository layer.
59
60## Testing Repository Implementations
61
62Repository implementations like `DrizzleAnnotationFieldRepository` and `DrizzleAnnotationTemplateRepository` require special consideration as they interact with databases:
63
64### Approach 1: In-memory Database (Recommended for Unit Tests)
65
66For fast, isolated tests of repository implementations:
67
681. **Use SQLite in-memory database:**
69
70 ```typescript
71 import { drizzle } from 'drizzle-orm/better-sqlite3';
72 import Database from 'better-sqlite3';
73 import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
74
75 // Setup in-memory SQLite for tests
76 const sqlite = new Database(':memory:');
77 const db = drizzle(sqlite);
78
79 // Apply migrations or create schema
80 migrate(db, { migrationsFolder: './drizzle' });
81
82 // Create repository with in-memory database
83 const repo = new DrizzleAnnotationFieldRepository(db);
84 ```
85
862. **Benefits:**
87 - Fast execution - no network or disk I/O
88 - Isolated - each test gets a fresh database
89 - No external dependencies or setup required
90
913. **Limitations:**
92 - SQLite dialect differences from PostgreSQL
93 - Some PostgreSQL-specific features won't be testable
94
95### Approach 2: Test Containers (For Integration Tests)
96
97For more realistic tests that verify PostgreSQL compatibility:
98
991. **Use testcontainers-node to spin up a PostgreSQL container:**
100
101 ```typescript
102 import { PostgreSqlContainer } from 'testcontainers';
103 import postgres from 'postgres';
104 import { drizzle } from 'drizzle-orm/postgres-js';
105
106 // In beforeAll hook
107 const container = await new PostgreSqlContainer().start();
108 const connectionString = container.getConnectionUri();
109 const sql = postgres(connectionString);
110 const db = drizzle(sql);
111
112 // In afterAll hook
113 await container.stop();
114 ```
115
1162. **Benefits:**
117 - Tests against actual PostgreSQL
118 - Verifies dialect-specific features
119 - Isolated from development/production databases
120
1213. **Drawbacks:**
122 - Slower test execution
123 - Requires Docker
124
125### Approach 3: Mock Database Client (For Pure Unit Tests)
126
127For pure unit tests focusing on repository logic:
128
1291. **Mock the database client:**
130
131 ```typescript
132 const mockDb = {
133 select: jest.fn().mockReturnThis(),
134 from: jest.fn().mockReturnThis(),
135 where: jest.fn().mockReturnThis(),
136 limit: jest.fn().mockReturnValue([
137 {
138 /* mock data */
139 },
140 ]),
141 insert: jest.fn().mockReturnThis(),
142 values: jest.fn().mockReturnThis(),
143 onConflictDoUpdate: jest.fn().mockResolvedValue(undefined),
144 delete: jest.fn().mockReturnThis(),
145 };
146
147 const repo = new DrizzleAnnotationFieldRepository(mockDb as any);
148 ```
149
1502. **Benefits:**
151 - Fastest execution
152 - No database dependencies
153 - Tests repository logic in isolation
154
1553. **Drawbacks:**
156 - Doesn't test actual SQL generation
157 - Requires complex mocking setup
158 - Lower confidence in database interaction
159
160### Recommended Approach
161
162Use a combination:
163
1641. **Unit tests with SQLite in-memory** for fast feedback during development
1652. **Integration tests with test containers** for critical paths and PostgreSQL-specific features
1663. **End-to-end tests** that verify the entire stack works together
167
168This provides a balance of speed, isolation, and confidence in your repository implementations.