Files
pos-system/docs/en/skills/testing-patterns.md
Ho Ngoc Hai 9b6c585f57 Enhance documentation structure and improve bilingual support across skills
- Updated skill documentation files to include structured metadata for better organization.
- Enhanced bilingual descriptions and guidelines for clarity in both English and Vietnamese.
- Refined sections on usage, best practices, and related skills to ensure consistency across all documentation.
- Improved formatting and removed outdated references to streamline the documentation experience.
- Added best practices checklists to relevant skills for better usability and adherence to standards.
2026-01-01 07:35:44 +07:00

11 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)

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();
});

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

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