---
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<br/>Fewest<br/>5-10s each<br/>Full stack"]
        Integration["Integration Tests<br/>Medium<br/>1-5s each<br/>Components"]
        Unit["Unit Tests<br/>Most<br/>&lt;1s each<br/>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: ['<rootDir>/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: ['<rootDir>/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<PrismaClient>()
}));

// 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<br/>Run once before all tests]
    BeforeAll --> BeforeEach[beforeEach<br/>Run before each test]
    BeforeEach --> Test[Execute Test<br/>Arrange-Act-Assert]
    Test --> AfterEach[afterEach<br/>Run after each test]
    AfterEach --> MoreTests{More Tests?}
    MoreTests -->|Yes| BeforeEach
    MoreTests -->|No| AfterAll[afterAll<br/>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<br/>- Database<br/>- External APIs<br/>- Services<br/>- Repositories]
        UnitCode --> UnitMock
    end
    
    subgraph IntegrationTests["Integration Tests"]
        IntCode[Application Code]
        IntReal[Real Internal Components]
        IntMock[External Dependencies Mocked<br/>- External APIs<br/>- Third-party Services]
        IntCode --> IntReal
        IntReal --> IntMock
    end
    
    subgraph E2ETests["E2E Tests"]
        E2ECode[Application Code]
        E2EReal[Real Internal Stack<br/>- Database<br/>- Services<br/>- Middleware]
        E2EMock[Only External Services Mocked<br/>- Payment APIs<br/>- Email Services<br/>- 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<PrismaClient>();

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<typeof axios>;

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