Files
pos-system/docs/en/skills/testing-patterns.md
Ho Ngoc Hai 2640b351c3 Enhance documentation with detailed diagrams and structured flows
- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes.
- Introduced configuration loading flow in configuration-management skill to clarify the configuration process.
- Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers.
- Enhanced various skills with additional diagrams to improve understanding of complex concepts.

These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
2026-01-01 23:22:54 +07:00

14 KiB

name, description
name description
testing-patterns 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.

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

// 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

// 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.

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

// 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

// 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

// 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.

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

// __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

// __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

// 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

// 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

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

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

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

// 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

// .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