# Testing Guidelines ## Overview This document defines the testing strategy, standards, and best practices for this workspace. All contributors must follow these guidelines to ensure consistent, reliable, and maintainable test coverage across the microservices architecture. ## Testing Pyramid ``` /\ / \ / E2E \ ~10% — Slow, expensive, high confidence /--------\ /Integration\ ~20% — Service boundaries, data flow /--------------\ / Unit Tests \ ~70% — Fast, isolated, many cases /--------------------\ ``` Follow the pyramid: write many unit tests, a moderate number of integration tests, and few E2E tests. Avoid the "ice cream cone" anti-pattern (many E2E, few unit tests). ## Test Categories ### 1. Unit Tests **Scope**: Individual functions, methods, classes — isolated from external dependencies. **Speed**: Fast (< 100ms per test). **Isolation**: Mock/stub all external dependencies (database, HTTP calls, message queue). ```typescript describe('UserService', () => { let userService: UserService; let mockRepository: jest.Mocked; beforeEach(() => { mockRepository = { findById: jest.fn(), findByEmail: jest.fn(), save: jest.fn(), update: jest.fn(), delete: jest.fn(), }; userService = new UserService(mockRepository, mockEmailService); }); describe('registerUser', () => { it('should create user with valid registration data', async () => { // Arrange const registrationData = { name: 'John', email: 'john@example.com', password: 'StrongP@ss1' }; mockRepository.findByEmail.mockResolvedValue(null); mockRepository.save.mockResolvedValue({ id: 1, ...registrationData }); // Act const result = await userService.registerUser(registrationData); // Assert expect(result.id).toBe(1); expect(mockRepository.findByEmail).toHaveBeenCalledWith('john@example.com'); expect(mockRepository.save).toHaveBeenCalledTimes(1); }); it('should throw ConflictError when email already exists', async () => { // Arrange const registrationData = { name: 'John', email: 'existing@example.com', password: 'StrongP@ss1' }; mockRepository.findByEmail.mockResolvedValue({ id: 2, email: 'existing@example.com' }); // Act & Assert await expect(userService.registerUser(registrationData)).rejects.toThrow(ConflictError); expect(mockRepository.save).not.toHaveBeenCalled(); }); }); }); ``` #### Unit Test Rules - Use the **Arrange-Act-Assert** pattern consistently. - One assertion per logical concept (multiple assertions on the same output are acceptable). - Do not test implementation details (private methods, internal state). Test behavior. - Do not mock the system under test — mock its dependencies only. - Every mock must be verified: unused mocks signal missing coverage or over-mocking. ### 2. Integration Tests **Scope**: Service boundaries, database interactions, API endpoints, message queue consumption. **Speed**: Moderate (100ms — 5s per test). **Isolation**: Use real database (test container), real message queue (in-memory broker), mock only external third-party services. ```typescript describe('OrderService Integration', () => { let app: Application; let db: TestDatabase; beforeAll(async () => { db = await TestDatabase.create(); app = await Application.create({ database: db.connection }); }); afterAll(async () => { await db.close(); await app.close(); }); beforeEach(async () => { await db.cleanAllTables(); }); describe('POST /api/v1/orders', () => { it('should create order and reserve inventory', async () => { // Seed product with stock await db.seed('products', { id: 1, sku: 'ELEC-001', stockQuantity: 10, price: 129.99 }); const response = await app.request .post('/api/v1/orders') .set('Authorization', `Bearer ${authToken}`) .send({ items: [{ productId: 1, quantity: 2 }] }); expect(response.status).toBe(201); expect(response.body.orderNumber).toMatch(/^ORD-/); // Verify stock was decremented const product = await db.query('SELECT stock_quantity FROM products WHERE id = 1'); expect(product.rows[0].stock_quantity).toBe(8); }); it('should return 409 when stock is insufficient', async () => { await db.seed('products', { id: 1, sku: 'ELEC-001', stockQuantity: 1, price: 129.99 }); const response = await app.request .post('/api/v1/orders') .set('Authorization', `Bearer ${authToken}`) .send({ items: [{ productId: 1, quantity: 5 }] }); expect(response.status).toBe(409); expect(response.body.error).toContain('insufficient stock'); }); }); }); ``` #### Integration Test Rules - Use a dedicated test database — never run against production or shared dev databases. - Clean all tables between tests (or use transaction rollback). - Verify actual data state in the database after operations. - Test error paths: constraint violations, concurrency conflicts, missing data. - For microservice communication: test request/response contracts, not internal logic of other services. ### 3. End-to-End (E2E) Tests **Scope**: Full user workflows crossing multiple services. **Speed**: Slow (5s — 30s per test). **Isolation**: Full running environment with real services. ```typescript describe('Order Checkout E2E', () => { it('should complete full checkout flow from browsing to payment', async () => { // 1. Browse products const products = await browseProducts({ category: 'electronics' }); expect(products.length).toBeGreaterThan(0); // 2. Add to cart const cart = await addToCart(products[0].id, quantity: 1); expect(cart.items.length).toBe(1); // 3. Checkout const order = await checkout(cart.id, { deliveryAddress: testAddress, paymentMethod: 'stripe', }); expect(order.status).toBe('PROCESSING'); // 4. Payment confirmation const payment = await confirmPayment(order.id); expect(payment.status).toBe('COMPLETED'); // 5. Verify final state const finalOrder = await getOrder(order.id); expect(finalOrder.status).toBe('CONFIRMED'); }); }); ``` #### E2E Test Rules - Cover only critical user journeys (registration, checkout, payment). - Keep E2E tests independent — use unique test data, clean up after tests. - Do not duplicate scenarios already covered by unit/integration tests. - Run E2E tests in CI only after unit and integration tests pass. ## Test Naming Convention Follow the pattern: `should [expected behavior] when [condition]` ```typescript // Good it('should return user when valid ID is provided'); it('should throw ConflictError when email already exists'); it('should return 404 when product is not found'); it('should decrement stock when order is created'); // Bad it('works'); it('test user'); it('handles error'); it('user service test 1'); ``` ## Test Organization ### File Structure ``` src/ services/ user.service.ts user.service.spec.ts ← Unit tests (same directory) controllers/ user.controller.ts user.controller.spec.ts ← Unit tests repositories/ user.repository.ts user.repository.integration.spec.ts ← Integration tests (suffix) tests/ integration/ ← Integration test helpers/setup test-database.ts test-app.ts e2e/ ← E2E tests (separate directory) checkout.e2e.spec.ts registration.e2e.spec.ts fixtures/ ← Shared test data users.ts products.ts ``` ### Naming Convention | Type | Pattern | Example | |------|---------|---------| | Unit | `*.spec.ts` | `user.service.spec.ts` | | Integration | `*.integration.spec.ts` | `order.repository.integration.spec.ts` | | E2E | `*.e2e.spec.ts` | `checkout.e2e.spec.ts` | ## What to Test ### Must Test 1. **Business logic** — all service methods, calculations, state transitions 2. **API contracts** — request/response shapes, status codes, error formats 3. **Data validation** — input validation rules, constraint enforcement 4. **Error handling** — expected exceptions, error messages, recovery paths 5. **Authorization** — role-based access, permission checks, forbidden operations 6. **Data integrity** — concurrent updates, cascading deletes, unique constraints ### Do Not Test 1. **Framework/library internals** — ORM query generation, Express routing 2. **Private methods** — test through public interface instead 3. **Trivial code** — simple getters/setters, property assignments 4. **Third-party service internals** — mock the external API boundary, not Stripe's logic ## Test Data Management ### Fixtures ```typescript // tests/fixtures/users.ts export const validUser = { name: 'John Doe', email: 'john.doe@example.com', password: 'StrongP@ss1!', }; export const duplicateEmailUser = { name: 'Jane Doe', email: 'john.doe@example.com', // same email as validUser password: 'AnotherP@ss2!', }; // tests/fixtures/products.ts export const electronicProduct = { sku: 'ELEC-001', name: 'Wireless Bluetooth Headphones', price: 129.99, stockQuantity: 50, categoryId: 5, }; ``` ### Test Database Seeding ```typescript class TestDatabase { async seed(table: string, data: Record): Promise { await this.connection(table).insert(data); } async seedMultiple(table: string, data: Record[]): Promise { await this.connection(table).insert(data); } async cleanAllTables(): Promise { const tables = ['order_items', 'orders', 'products', 'categories', 'user_profiles', 'users']; for (const table of tables) { await this.connection(table).del(); } } } ``` ## Testing Across Services ### Service Boundary Testing For each inter-service communication, verify: 1. **Request contract** — the shape of requests sent to other services 2. **Response contract** — the shape of responses expected from other services 3. **Error contract** — how errors from other services are handled 4. **Timeout handling** — circuit breaker behavior, fallback responses ```typescript describe('OrderService → PaymentService boundary', () => { it('should handle PaymentService timeout with circuit breaker', async () => { mockPaymentClient.processPayment.mockImplementation( () => new Promise((_, reject) => setTimeout(() => reject(new TimeoutError()), 3000)) ); // Circuit breaker should open after threshold failures await expect(orderService.processPayment(order)).rejects.toThrow(CircuitBreakerOpenError); }); it('should retry with exponential backoff on transient failures', async () => { mockPaymentClient.processPayment .mockRejectedValueOnce(new TransientError()) .mockResolvedValueOnce({ status: 'SUCCESS' }); const result = await orderService.processPayment(order); expect(result.status).toBe('SUCCESS'); expect(mockPaymentClient.processPayment).toHaveBeenCalledTimes(2); }); }); ``` ### Contract Testing Use contract tests to verify API compatibility between services without running both services. ```typescript // Consumer contract (Order Service expects this from Payment Service) describe('Payment Service API contract', () => { it('should match expected response schema', () => { const response = paymentClient.processPayment(testPaymentData); expect(response).toMatchObject({ id: expect.any(String), status: expect.stringMatching(/SUCCESS|FAILED|PENDING/), amount: expect.any(Number), processedAt: expect.any(Date), }); }); }); ``` ## Validation Testing Test all validation rules defined in the data models. ```typescript describe('User Validation', () => { it('should reject name shorter than 2 characters', () => { expect(() => validateUser({ name: 'J', email: 'valid@example.com' })).toThrow(ValidationError); }); it('should reject invalid email format', () => { expect(() => validateUser({ name: 'John', email: 'invalid-email' })).toThrow(ValidationError); }); it('should reject password without special character', () => { expect(() => validateUser({ name: 'John', email: 'valid@example.com', password: 'NoSpecial1' })).toThrow(ValidationError); }); it('should accept valid user data', () => { expect(() => validateUser(validUser)).not.toThrow(); }); }); describe('Product Validation', () => { it('should reject SKU with wrong format', () => { expect(() => validateProduct({ sku: 'invalid', name: 'Product', price: 10 })).toThrow(ValidationError); }); it('should reject negative price', () => { expect(() => validateProduct({ ...electronicProduct, price: -1 })).toThrow(ValidationError); }); it('should reject non-integer stock quantity', () => { expect(() => validateProduct({ ...electronicProduct, stockQuantity: 5.5 })).toThrow(ValidationError); }); }); ``` ## Error Path Testing Every error path must be tested. For each service method: ```typescript describe('OrderService.cancelOrder', () => { it('should cancel pending order and release inventory', async () => { /* ... */ }); it('should throw NotFoundError when order does not exist', async () => { /* ... */ }); it('should throw ConflictError when order is already delivered', async () => { /* ... */ }); it('should throw ConflictError when order is already cancelled', async () => { /* ... */ }); it('should rollback inventory release if status update fails', async () => { /* ... */ }); }); ``` ## Performance and Load Testing ### Performance Test Guidelines - Set measurable thresholds for API response times. - Test under expected load, not just single-request scenarios. - Identify bottlenecks before production deployment. | Endpoint | Target p50 | Target p95 | Target p99 | |----------|-----------|-----------|-----------| | GET /products | < 50ms | < 200ms | < 500ms | | POST /orders | < 100ms | < 500ms | < 1000ms | | GET /orders/:id | < 30ms | < 100ms | < 300ms | ### Concurrency Testing ```typescript describe('Inventory concurrency', () => { it('should prevent overselling when multiple orders hit same product', async () => { // Product has stock of 5 await db.seed('products', { id: 1, stockQuantity: 5, price: 100 }); // 10 concurrent orders requesting 1 unit each const results = await Promise.allSettled( Array.from({ length: 10 }, () => orderService.createOrder({ items: [{ productId: 1, quantity: 1 }] }) ) ); const successful = results.filter(r => r.status === 'fulfilled'); expect(successful.length).toBe(5); // Only 5 should succeed const product = await db.query('SELECT stock_quantity FROM products WHERE id = 1'); expect(product.rows[0].stock_quantity).toBe(0); }); }); ``` ## Code Coverage ### Minimum Coverage Targets | Layer | Line Coverage | Branch Coverage | |-------|--------------|----------------| | Services (business logic) | 90% | 85% | | Controllers (API) | 80% | 75% | | Repositories (data access) | 70% | 60% | | Overall project | 80% | 70% | ### Coverage Rules - Coverage is a signal, not a goal. 100% coverage with poor tests is worse than 80% with meaningful tests. - Do not write tests solely to increase coverage numbers. - Uncovered branches in business logic require justification (documented in code comments). - Coverage reports must be generated in CI and posted as PR checks. ## Mocking Guidelines ### When to Mock - External HTTP services (Payment Gateway, Email, SMS) - Third-party SDKs and libraries with side effects - Time-dependent logic (clocks, timers) - File system operations - Message queue publishing (verify the message was published, not consumed) ### When NOT to Mock - Internal service-to-service calls in integration tests - Database operations in integration tests (use real test database) - Utility functions with pure logic (test them directly) - Framework-provided mechanisms (use framework test utilities) ### Mock Best Practices ```typescript // Good: mock returns realistic data matching the interface mockRepository.findByEmail.mockResolvedValue({ id: 1, name: 'John Doe', email: 'john@example.com', }); // Bad: mock returns incomplete or unrealistic data mockRepository.findByEmail.mockResolvedValue({}); // empty object mockRepository.findByEmail.mockResolvedValue(null); // always null hides bugs // Good: verify mock was called with correct arguments expect(mockRepository.save).toHaveBeenCalledWith({ name: 'John', email: 'john@example.com', passwordHash: expect.any(String), }); // Good: verify mock call count expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledTimes(1); // Bad: over-mocking — mocking the system under test jest.spyOn(userService, 'hashPassword'); // don't mock internal methods ``` ## CI Pipeline Integration ### Pipeline Stages ```yaml # CI Pipeline order stages: 1. Lint and format check 2. Unit tests ← Fast gate, must pass before merge 3. Integration tests ← Run on real test database 4. Build verification 5. E2E tests ← Run only on staging environment 6. Security scan 7. Coverage report ← Post as PR comment ``` ### Pre-commit Hooks - Run linter and formatter - Run unit tests for changed files only - Validate commit messages ### CI Failure Handling - Unit test failure → Block merge, fix immediately - Integration test failure → Block merge, investigate before fix - E2E test failure → Notify team, investigate (may be environment issue) - Coverage drop below threshold → Block merge, add missing tests ## Language-Specific Test Frameworks ### TypeScript/JavaScript - **Framework**: Jest - **Assertion**: Jest built-in (`expect`) - **Mocking**: Jest mock functions (`jest.fn()`, `jest.spyOn()`) - **Integration**: Supertest for HTTP, test containers for database - **E2E**: Playwright or Cypress ```json // jest.config.js { "testMatch": ["**/*.spec.ts"], "collectCoverageFrom": ["src/**/*.ts", "!src/**/*.spec.ts"], "coverageThreshold": { "global": { "lines": 80, "branches": 70 } } } ``` ### Java - **Framework**: JUnit 5 - **Assertion**: AssertJ - **Mocking**: Mockito - **Integration**: Spring Boot Test, TestContainers - **E2E**: REST Assured ```java // JUnit 5 example @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void shouldThrowConflictErrorWhenEmailAlreadyExists() { when(userRepository.findByEmail("existing@example.com")) .thenReturn(Optional.of(existingUser)); assertThatThrownBy(() -> userService.registerUser(registrationData)) .isInstanceOf(ConflictError.class); } } ``` ### Python - **Framework**: pytest - **Assertion**: Python built-in `assert` - **Mocking**: unittest.mock (`mock`, `patch`) - **Integration**: pytest fixtures with real database - **E2E**: pytest + requests or Playwright ```python # pytest example class TestUserService: @pytest.fixture def user_service(self, mock_repository): return UserService(mock_repository) def test_should_raise_conflict_when_email_exists(self, user_service): mock_repository.find_by_email.return_value = existing_user with pytest.raises(ConflictError): user_service.register_user(registration_data) ``` ## Debugging Failed Tests ### Common Failure Patterns | Pattern | Cause | Fix | |---------|-------|-----| | Flaky test (passes sometimes) | Shared state, timing dependency, ordering | Isolate test data, use deterministic mocks | | Mock not called | Wrong mock setup, async issue | Verify mock registration, check async handling | | Unexpected null | Over-mocking, incomplete fixture | Use realistic test data, verify mock returns | | Timeout in integration test | Database lock, slow query | Use transaction isolation, check indexes | | E2E test fails in CI only | Environment difference, service not ready | Add health checks, increase timeouts for CI | ### Test Debugging Steps 1. Run the single failing test in isolation 2. Check test data and fixture setup 3. Verify mock configuration matches actual interface 4. Add diagnostic logs (do not leave them in committed code) 5. Check for hidden dependencies (env vars, file paths, time) --- *These testing guidelines should be adapted based on the specific project requirements and team capabilities. Regular reviews and updates are encouraged.*