--- name: testing-patterns description: Testing best practices for GoodGo microservices. Use when writing unit tests, integration tests, E2E tests, setting up Jest, mocking dependencies, or debugging test failures. --- # Testing Patterns for GoodGo Microservices ## When to Use This Skill Use this skill when: - Writing unit tests for services, controllers, or repositories - Creating integration tests for middleware chains - Building E2E tests for API endpoints - Setting up Jest configuration for a new service - Mocking external dependencies (Prisma, Redis, Auth SDK) - Debugging test failures - Improving test coverage ## Core Concepts ### Test Types 1. **Unit Tests**: Test individual functions/classes in isolation - Location: Next to source files (`*.test.ts`) - Scope: Single function or class - Dependencies: Mocked - Speed: Fast (<1s per test) 2. **Integration Tests**: Test component interactions - Location: `__tests__/` directory - Scope: Multiple components working together - Dependencies: Some real, some mocked - Speed: Medium (1-5s per test) 3. **E2E Tests**: Test complete request/response cycles - Location: `__tests__/*.e2e.ts` - Scope: Full API workflow - Dependencies: Test database, mocked external services - Speed: Slow (5-10s per test) ### Testing Pyramid The testing pyramid illustrates the recommended distribution of test types. Most tests should be fast unit tests, with fewer integration tests, and the fewest E2E tests. ```mermaid graph TD subgraph pyramid["Testing Pyramid"] E2E["E2E Tests
Fewest
5-10s each
Full stack"] Integration["Integration Tests
Medium
1-5s each
Components"] Unit["Unit Tests
Most
<1s each
Isolated"] end Unit --> Integration Integration --> E2E style E2E fill:#e1f5ff style Integration fill:#b3e5fc style Unit fill:#81d4fa ``` **Key Principles:** - **Unit Tests (Base)**: Many fast tests covering individual functions/classes - **Integration Tests (Middle)**: Fewer tests verifying component interactions - **E2E Tests (Top)**: Fewest tests validating complete workflows ## Jest Configuration ```typescript // jest.config.ts import type { Config } from 'jest'; const config: Config = { preset: 'ts-jest', testEnvironment: 'node', roots: ['/src'], testMatch: [ '**/__tests__/**/*.test.ts', '**/__tests__/**/*.e2e.ts', '**/?(*.)+(spec|test).ts' ], collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/main.ts', '!src/config/**/*.ts' ], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], coverageThreshold: { global: { branches: 70, functions: 70, lines: 70, statements: 70 } }, setupFilesAfterEnv: ['/src/__tests__/setupTests.ts'], testTimeout: 10000, clearMocks: true }; export default config; ``` ## Setup Files ```typescript // src/__tests__/setupTests.ts import { mockDeep, mockReset } from 'jest-mock-extended'; import { PrismaClient } from '@prisma/client'; // Mock Prisma jest.mock('../prisma', () => ({ __esModule: true, default: mockDeep() })); // Mock Redis jest.mock('ioredis', () => { const Redis = jest.requireActual('ioredis-mock'); return Redis; }); // Global test utilities global.testUtils = { generateId: () => `test-${Date.now()}`, createMockRequest: () => ({ headers: {}, body: {}, query: {}, params: {} }), createMockResponse: () => { const res: any = {}; res.status = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); res.send = jest.fn().mockReturnValue(res); return res; } }; beforeEach(() => { jest.clearAllMocks(); }); ``` ### Test Execution Flow The test execution flow shows the lifecycle hooks and execution order in Jest test suites. ```mermaid flowchart TD Start([Test Suite Starts]) --> BeforeAll[beforeAll
Run once before all tests] BeforeAll --> BeforeEach[beforeEach
Run before each test] BeforeEach --> Test[Execute Test
Arrange-Act-Assert] Test --> AfterEach[afterEach
Run after each test] AfterEach --> MoreTests{More Tests?} MoreTests -->|Yes| BeforeEach MoreTests -->|No| AfterAll[afterAll
Run once after all tests] AfterAll --> End([Test Suite Ends]) style Start fill:#c8e6c9 style End fill:#c8e6c9 style Test fill:#fff9c4 style BeforeAll fill:#e1bee7 style AfterAll fill:#e1bee7 style BeforeEach fill:#bbdefb style AfterEach fill:#bbdefb ``` **Execution Order:** 1. `beforeAll`: Setup that runs once for the entire suite (e.g., database connection) 2. `beforeEach`: Setup that runs before each test (e.g., reset mocks, create test data) 3. **Test Execution**: The actual test code following AAA pattern 4. `afterEach`: Cleanup after each test (e.g., clear mocks, reset state) 5. `afterAll`: Final cleanup after all tests (e.g., close database connection) ## Testing Patterns ### Unit Test Pattern ```typescript // feature.service.test.ts import { FeatureService } from './feature.service'; import { mockDeep } from 'jest-mock-extended'; describe('FeatureService', () => { let service: FeatureService; let mockRepository: any; beforeEach(() => { mockRepository = { findById: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn() }; service = new FeatureService(mockRepository); }); describe('findById', () => { it('should return feature when found', async () => { // Arrange const mockFeature = { id: '1', name: 'Test Feature' }; mockRepository.findById.mockResolvedValue(mockFeature); // Act const result = await service.findById('1'); // Assert expect(result).toEqual(mockFeature); expect(mockRepository.findById).toHaveBeenCalledWith('1'); }); it('should throw error when feature not found', async () => { // Arrange mockRepository.findById.mockResolvedValue(null); // Act & Assert await expect(service.findById('999')).rejects.toThrow('Feature not found'); }); }); }); ``` ### Integration Test Pattern ```typescript // auth.middleware.test.ts import { authMiddleware } from '../auth.middleware'; import { createMockRequest, createMockResponse } from '../../test-utils'; import jwt from 'jsonwebtoken'; describe('Auth Middleware', () => { const mockNext = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); it('should call next when valid token provided', async () => { // Arrange const req = createMockRequest(); const res = createMockResponse(); const token = jwt.sign({ userId: '123' }, 'secret'); req.headers.authorization = `Bearer ${token}`; // Act await authMiddleware(req, res, mockNext); // Assert expect(mockNext).toHaveBeenCalled(); expect(req.user).toEqual({ userId: '123' }); }); it('should return 401 when no token provided', async () => { // Arrange const req = createMockRequest(); const res = createMockResponse(); // Act await authMiddleware(req, res, mockNext); // Assert expect(res.status).toHaveBeenCalledWith(401); expect(mockNext).not.toHaveBeenCalled(); }); }); ``` ### E2E Test Pattern ```typescript // feature.e2e.ts import supertest from 'supertest'; import { createApp } from '../app'; import { prisma } from '../prisma'; describe('Feature API E2E', () => { let app: any; let request: any; beforeAll(async () => { app = await createApp(); request = supertest(app); }); afterAll(async () => { await prisma.$disconnect(); }); beforeEach(async () => { await prisma.feature.deleteMany(); }); describe('POST /api/features', () => { it('should create a new feature', async () => { // Arrange const featureData = { name: 'New Feature', description: 'Feature description' }; // Act const response = await request .post('/api/features') .set('Authorization', 'Bearer valid-token') .send(featureData) .expect(201); // Assert expect(response.body).toMatchObject({ success: true, data: { name: 'New Feature', description: 'Feature description' } }); const created = await prisma.feature.findFirst({ where: { name: 'New Feature' } }); expect(created).toBeDefined(); }); }); }); ``` ## Mocking Strategies ### Mocking Strategy by Test Type Different test types require different levels of mocking. This diagram shows what should be mocked at each test level. ```mermaid graph TB subgraph UnitTests["Unit Tests"] UnitCode[Application Code] UnitMock[All Dependencies Mocked
- Database
- External APIs
- Services
- Repositories] UnitCode --> UnitMock end subgraph IntegrationTests["Integration Tests"] IntCode[Application Code] IntReal[Real Internal Components] IntMock[External Dependencies Mocked
- External APIs
- Third-party Services] IntCode --> IntReal IntReal --> IntMock end subgraph E2ETests["E2E Tests"] E2ECode[Application Code] E2EReal[Real Internal Stack
- Database
- Services
- Middleware] E2EMock[Only External Services Mocked
- Payment APIs
- Email Services
- Third-party APIs] E2ECode --> E2EReal E2EReal --> E2EMock end style UnitTests fill:#ffcdd2 style IntegrationTests fill:#fff9c4 style E2ETests fill:#c8e6c9 style UnitMock fill:#f8bbd0 style IntMock fill:#ffe082 style E2EMock fill:#a5d6a7 ``` **Mocking Guidelines:** - **Unit Tests**: Mock all external dependencies to test logic in isolation - **Integration Tests**: Use real internal components, mock only external services - **E2E Tests**: Use real database and internal stack, mock only third-party external services ### Mock Prisma ```typescript // __mocks__/prisma.ts import { PrismaClient } from '@prisma/client'; import { mockDeep, DeepMockProxy } from 'jest-mock-extended'; export const prismaMock = mockDeep(); jest.mock('../src/prisma', () => ({ __esModule: true, default: prismaMock, })); // Usage in tests import { prismaMock } from '../__mocks__/prisma'; test('should create user', async () => { const user = { id: '1', email: 'test@example.com' }; prismaMock.user.create.mockResolvedValue(user); const result = await createUser({ email: 'test@example.com' }); expect(result).toEqual(user); }); ``` ### Mock Redis ```typescript // __mocks__/redis.ts import Redis from 'ioredis-mock'; export const redisMock = new Redis(); // Usage in tests test('should cache value', async () => { const cache = new CacheService(redisMock); await cache.set('key', 'value'); const result = await cache.get('key'); expect(result).toBe('value'); }); ``` ### Mock External APIs ```typescript // Mock axios jest.mock('axios'); import axios from 'axios'; const mockedAxios = axios as jest.Mocked; test('should fetch external data', async () => { mockedAxios.get.mockResolvedValue({ data: { result: 'success' } }); const result = await fetchExternalData(); expect(result).toEqual({ result: 'success' }); }); ``` ## Testing Utilities ```typescript // test-utils.ts export class TestFactory { static createUser(overrides = {}) { return { id: 'test-user-1', email: 'test@example.com', name: 'Test User', createdAt: new Date(), ...overrides }; } static createAuthToken(userId: string) { return jwt.sign({ userId }, 'test-secret'); } static async cleanDatabase() { await prisma.user.deleteMany(); await prisma.feature.deleteMany(); } } // Usage const user = TestFactory.createUser({ name: 'Custom Name' }); const token = TestFactory.createAuthToken(user.id); ``` ## Common Test Scenarios ### Testing Error Handling ```typescript test('should handle database errors gracefully', async () => { prismaMock.user.findUnique.mockRejectedValue( new Error('Database connection failed') ); const response = await request .get('/api/users/123') .expect(500); expect(response.body).toEqual({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } }); }); ``` ### Testing Validation ```typescript describe('Validation', () => { it('should reject invalid email', async () => { const response = await request .post('/api/auth/register') .send({ email: 'invalid-email', password: '123456' }) .expect(400); expect(response.body.error.code).toBe('VALIDATION_ERROR'); }); }); ``` ### Testing Pagination ```typescript test('should paginate results', async () => { // Create test data const items = Array(25).fill(null).map((_, i) => ({ id: `item-${i}`, name: `Item ${i}` })); prismaMock.item.findMany.mockResolvedValue(items.slice(0, 10)); prismaMock.item.count.mockResolvedValue(25); const response = await request .get('/api/items?page=1&limit=10') .expect(200); expect(response.body).toEqual({ success: true, data: items.slice(0, 10), pagination: { page: 1, limit: 10, total: 25, totalPages: 3 } }); }); ``` ## Test Commands ```json // package.json { "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "test:unit": "jest --testPathPattern=\\.test\\.ts$", "test:e2e": "jest --testPathPattern=\\.e2e\\.ts$", "test:ci": "jest --coverage --silent --maxWorkers=2" } } ``` ## Debugging Tests ### Debug with VS Code ```json // .vscode/launch.json { "configurations": [ { "type": "node", "request": "launch", "name": "Debug Jest Tests", "runtimeExecutable": "npm", "runtimeArgs": ["test", "--", "--runInBand"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" } ] } ``` ### Debug Tips 1. Use `test.only()` to run single test 2. Use `--detectOpenHandles` for async issues 3. Use `--runInBand` for sequential execution 4. Add `console.log()` statements temporarily 5. Use debugger breakpoints in VS Code ## Best Practices Checklist - [ ] Each test is independent and isolated - [ ] Tests follow AAA pattern (Arrange-Act-Assert) - [ ] Mock external dependencies - [ ] Test edge cases and error scenarios - [ ] Keep tests simple and focused - [ ] Use descriptive test names - [ ] Maintain >70% code coverage - [ ] Run tests before committing - [ ] Keep test data realistic - [ ] Clean up after tests