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

589 lines
14 KiB
Markdown

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